oihana-next-ui 0.1.46 → 0.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.
- package/README.md +48 -26
- package/package.json +1 -1
- package/src/app/lab/@tabs/menus/page.js +14 -4
- package/src/app/lab/@tabs/modals/page.js +8 -5
- package/src/components/buttons/RefreshButton.jsx +6 -6
- package/src/components/modals/Modal.jsx +1 -1
- package/src/contexts/navigation/context.js +14 -1
- package/src/contexts/navigation/helpers/collapseStorage.js +129 -0
- package/src/contexts/navigation/helpers/constants.js +59 -0
- package/src/contexts/navigation/helpers/containsActivePath.js +54 -0
- package/src/contexts/navigation/helpers/findActiveAncestorIds.js +78 -0
- package/src/contexts/navigation/helpers/mapI18nItem.js +12 -9
- package/src/contexts/navigation/helpers/resolveCollapseOpen.js +62 -0
- package/src/contexts/navigation/provider.js +164 -12
- package/src/contexts/navigation/useNavigationCollapse.js +63 -0
- package/src/contexts/toasts/provider.js +168 -36
- package/src/demo/menus/CollapsePersistenceDemo.jsx +210 -0
- package/src/demo/modals/ToastOverModalDemo.jsx +260 -0
- package/src/display/Application.jsx +6 -2
- package/src/display/ui/navigation/Collapse.jsx +115 -10
- package/src/display/ui/navigation/Link.jsx +20 -6
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -100,50 +100,72 @@ Open [http://localhost:3666](http://localhost:3666) to browse the component demo
|
|
|
100
100
|
- [Day.js](https://day.js.org/) — Lightweight date library
|
|
101
101
|
- [Chroma.js](https://www.vis4.net/chromajs/) — Color manipulation library
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
## Release
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
bun run build:lib
|
|
107
|
-
```
|
|
105
|
+
The package publishes the raw `src/` tree (no build step) — see the `files` and `exports` fields in [`package.json`](./package.json).
|
|
108
106
|
|
|
109
|
-
###
|
|
107
|
+
### Versioning
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
bun run build:lib:watch
|
|
113
|
-
```
|
|
109
|
+
This project follows [Semantic Versioning](https://semver.org/) — `MAJOR.MINOR.PATCH` (e.g. `1.2.3`).
|
|
114
110
|
|
|
115
|
-
|
|
111
|
+
| Type | Command | Example | When to use |
|
|
112
|
+
|-------|-------------------------|---------------------|-----------------------------------------------|
|
|
113
|
+
| Patch | `bun run release:patch` | `0.1.46` → `0.1.47` | Bug fix, small tweak |
|
|
114
|
+
| Minor | `bun run release:minor` | `0.1.46` → `0.2.0` | New component or feature, backward compatible |
|
|
115
|
+
| Major | `bun run release:major` | `0.1.46` → `1.0.0` | Breaking change |
|
|
116
116
|
|
|
117
|
-
###
|
|
117
|
+
### Prerequisites
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
- Logged in to npm — `npm whoami` should print your username (otherwise `npm login`).
|
|
120
|
+
- A git remote named `origin-ssh` configured (the `release` script pushes there with `--follow-tags`).
|
|
121
|
+
- A clean working tree, ideally — `release:*` will otherwise commit any pending change as `chore: prepare release` before bumping the version.
|
|
122
|
+
|
|
123
|
+
### Patch release walkthrough — e.g. `0.1.46` → `0.1.47`
|
|
124
|
+
|
|
125
|
+
1. **Update [`CHANGELOG.md`](./CHANGELOG.md)** — add a new section under `[Unreleased]` with the new version and date :
|
|
126
|
+
|
|
127
|
+
~~~markdown
|
|
128
|
+
## [0.1.47] — 2026-04-27
|
|
129
|
+
|
|
130
|
+
**Components**
|
|
131
|
+
- `XYZ` — what changed and why.
|
|
132
|
+
~~~
|
|
120
133
|
|
|
121
|
-
|
|
122
|
-
|------|---------|---------|-------------|
|
|
123
|
-
| Patch | `bun run release:patch` | `0.1.0` → `0.1.1` | Bug fix, minor tweak |
|
|
124
|
-
| Minor | `bun run release:minor` | `0.1.0` → `0.2.0` | New component or feature, backward compatible |
|
|
125
|
-
| Major | `bun run release:major` | `0.1.0` → `1.0.0` | Breaking change |
|
|
134
|
+
2. **Run the release script** :
|
|
126
135
|
|
|
127
|
-
|
|
136
|
+
```bash
|
|
137
|
+
bun run release:patch
|
|
138
|
+
```
|
|
128
139
|
|
|
129
|
-
|
|
140
|
+
What happens, in order — all of this is automatic :
|
|
141
|
+
|
|
142
|
+
1. `stage` — commits any pending change as `chore: prepare release` (skipped if the working tree is clean).
|
|
143
|
+
2. `npm version patch` — bumps `0.1.46` → `0.1.47` in `package.json`.
|
|
144
|
+
3. `version` script (auto-run by `npm version`) :
|
|
145
|
+
- `inject-version` writes the new version into `src/version.js` and `public/sw.js`,
|
|
146
|
+
- `generate-exports` refreshes the `exports` field in `package.json`,
|
|
147
|
+
- then stages `src/version.js`, `public/sw.js` and `package.json` for the version commit.
|
|
148
|
+
4. `npm version` creates the release commit (`0.1.47`) and the matching git tag.
|
|
149
|
+
5. `postversion` script (auto-run by `npm version`) → `release` :
|
|
150
|
+
- `npm publish --access public` publishes to npm,
|
|
151
|
+
- `git push origin-ssh --follow-tags` pushes the commit and the tag to GitHub.
|
|
152
|
+
|
|
153
|
+
### Manual / pre-release version
|
|
154
|
+
|
|
155
|
+
Set a specific version manually — `version` + `postversion` still run as above :
|
|
130
156
|
|
|
131
157
|
```bash
|
|
132
158
|
npm version 1.0.0
|
|
133
|
-
bun run release
|
|
134
159
|
```
|
|
135
160
|
|
|
136
|
-
|
|
161
|
+
Pre-release versions :
|
|
137
162
|
|
|
138
163
|
```bash
|
|
139
|
-
npm version prerelease --preid=alpha # 0.1.
|
|
140
|
-
npm version prerelease --preid=beta # 0.1.
|
|
141
|
-
npm version prerelease --preid=rc # 0.1.
|
|
142
|
-
bun run release
|
|
164
|
+
npm version prerelease --preid=alpha # 0.1.46 → 0.1.47-alpha.0
|
|
165
|
+
npm version prerelease --preid=beta # 0.1.46 → 0.1.47-beta.0
|
|
166
|
+
npm version prerelease --preid=rc # 0.1.46 → 0.1.47-rc.0
|
|
143
167
|
```
|
|
144
168
|
|
|
145
|
-
`npm version` automatically updates `package.json`, creates a Git commit and a Git tag.
|
|
146
|
-
|
|
147
169
|
## License
|
|
148
170
|
|
|
149
171
|
[Mozilla Public License 2.0](./LICENSE) — © Marc Alcaraz
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client' ;
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
3
|
+
import CollapsePersistenceDemo from '@/demo/menus/CollapsePersistenceDemo';
|
|
4
|
+
import FlagMenuDemo from '@/demo/menus/FlagMenuDemo';
|
|
5
|
+
import MenuNavigationDemo from '@/demo/menus/MenuNavigationDemo';
|
|
6
|
+
import Container from '@/display/Container' ;
|
|
7
|
+
import Page from '@/display/Page' ;
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Toasts showcase page.
|
|
@@ -39,6 +40,15 @@ const ToastsShowcase = ({ path = 'app.test' }) =>
|
|
|
39
40
|
</div>
|
|
40
41
|
</Container>
|
|
41
42
|
|
|
43
|
+
{/* Collapse persistence Demo */}
|
|
44
|
+
<Container className="flex flex-col gap-4">
|
|
45
|
+
<h2 className="text-2xl font-bold">Collapse persistence</h2>
|
|
46
|
+
<p className="opacity-70">
|
|
47
|
+
NavigationProvider — defaultMode and storageKey props.
|
|
48
|
+
</p>
|
|
49
|
+
<CollapsePersistenceDemo />
|
|
50
|
+
</Container>
|
|
51
|
+
|
|
42
52
|
</Page>
|
|
43
53
|
) ;
|
|
44
54
|
} ;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client' ;
|
|
2
2
|
|
|
3
|
-
import CRUDDemo
|
|
4
|
-
import InputModalDemo
|
|
5
|
-
import ModalDemo
|
|
6
|
-
import
|
|
7
|
-
import
|
|
3
|
+
import CRUDDemo from '@/demo/modals/CRUDModalDemo';
|
|
4
|
+
import InputModalDemo from '@/demo/modals/InputModalDemo';
|
|
5
|
+
import ModalDemo from '@/demo/modals/ModalDemo';
|
|
6
|
+
import ToastOverModalDemo from '@/demo/modals/ToastOverModalDemo';
|
|
7
|
+
import Container from '@/display/Container';
|
|
8
|
+
import Page from '@/display/Page' ;
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Modal showcase page.
|
|
@@ -26,6 +27,8 @@ const ModalShowcase = ({ path = 'app.test' }) =>
|
|
|
26
27
|
|
|
27
28
|
<ModalDemo />
|
|
28
29
|
|
|
30
|
+
<ToastOverModalDemo />
|
|
31
|
+
|
|
29
32
|
<CRUDDemo />
|
|
30
33
|
|
|
31
34
|
<InputModalDemo />
|
|
@@ -38,13 +38,13 @@ import MotionButton from './MotionButton' ;
|
|
|
38
38
|
*/
|
|
39
39
|
const RefreshButton =
|
|
40
40
|
({
|
|
41
|
-
color
|
|
42
|
-
icon
|
|
43
|
-
motion
|
|
41
|
+
color = 'secondary' ,
|
|
42
|
+
icon = MdRefresh ,
|
|
43
|
+
motion = Jump ,
|
|
44
44
|
motionProps = { delay: 0.3 } ,
|
|
45
|
-
path
|
|
46
|
-
shape
|
|
47
|
-
size
|
|
45
|
+
path = 'components.buttons.refresh' ,
|
|
46
|
+
shape = 'circle' ,
|
|
47
|
+
size = 'md' ,
|
|
48
48
|
...rest
|
|
49
49
|
}) =>
|
|
50
50
|
(
|
|
@@ -4,6 +4,19 @@ import { createContext } from 'react' ;
|
|
|
4
4
|
* @typedef {Object} NavigationContextValue
|
|
5
5
|
* @property {Object[] | null} navigation - Current navigation items.
|
|
6
6
|
* @property {(value: Object[]) => void} setNavigation - Function to update navigation.
|
|
7
|
+
* @property {'open' | 'closed' | 'auto'} defaultMode - Default open/closed
|
|
8
|
+
* mode applied to collapse items that have no persisted state and no
|
|
9
|
+
* per-item `defaultOpen` override.
|
|
10
|
+
* @property {Record<string, boolean>} collapses - Per-id open/closed map
|
|
11
|
+
* reflecting explicit user choices (post-hydration).
|
|
12
|
+
* @property {(id: string, open: boolean) => void} setCollapse - Records an
|
|
13
|
+
* explicit user choice for a single collapse and persists it when a
|
|
14
|
+
* `storageKey` was supplied to the provider.
|
|
15
|
+
* @property {(id: string, item?: Object) => boolean} getCollapseOpen -
|
|
16
|
+
* Returns the effective open state for a collapse, applying the
|
|
17
|
+
* priority chain: persisted → auto(pathname) → item.defaultOpen → defaultMode.
|
|
18
|
+
* @property {string | null} pathname - Current pathname, captured by the
|
|
19
|
+
* provider so consumers (e.g. `Collapse`) don't have to read it again.
|
|
7
20
|
*/
|
|
8
21
|
|
|
9
22
|
/**
|
|
@@ -15,4 +28,4 @@ const NavigationContext = createContext(
|
|
|
15
28
|
|
|
16
29
|
NavigationContext.displayName = 'NavigationContext' ;
|
|
17
30
|
|
|
18
|
-
export default NavigationContext ;
|
|
31
|
+
export default NavigationContext ;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* localStorage helpers for the navigation collapse state.
|
|
3
|
+
*
|
|
4
|
+
* @module contexts/navigation/helpers/collapseStorage
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import notEmpty from 'vegas-js-core/src/strings/notEmpty' ;
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
COLLAPSE_STATE_ITEMS_KEY ,
|
|
11
|
+
COLLAPSE_STATE_VERSION ,
|
|
12
|
+
COLLAPSE_STATE_VERSION_KEY ,
|
|
13
|
+
} from './constants' ;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns true when `window.localStorage` can be read or written.
|
|
17
|
+
* Catches SSR (no `window`), Safari private mode, disabled storage,
|
|
18
|
+
* and any other host-thrown error.
|
|
19
|
+
*
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
const isStorageAvailable = () =>
|
|
23
|
+
{
|
|
24
|
+
try
|
|
25
|
+
{
|
|
26
|
+
return typeof window !== 'undefined'
|
|
27
|
+
&& typeof window.localStorage !== 'undefined' ;
|
|
28
|
+
}
|
|
29
|
+
catch
|
|
30
|
+
{
|
|
31
|
+
return false ;
|
|
32
|
+
}
|
|
33
|
+
} ;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reads the persisted collapse state for a given storage key.
|
|
37
|
+
*
|
|
38
|
+
* Returns an empty object when the key is missing/empty, when storage is
|
|
39
|
+
* unavailable (SSR, private mode, quota errors), when the payload is not
|
|
40
|
+
* JSON, or when the schema version does not match the current one.
|
|
41
|
+
*
|
|
42
|
+
* Never throws.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} [key] - Opaque storage key chosen by the consumer.
|
|
45
|
+
* @returns {Record<string, boolean>} Per-id open/closed map (may be empty).
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```js
|
|
49
|
+
* const state = loadCollapseState( 'my-app:nav:v1' ) ;
|
|
50
|
+
* // → { lab: true, layouts: false }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export const loadCollapseState = ( key ) =>
|
|
54
|
+
{
|
|
55
|
+
if ( !notEmpty( key ) || !isStorageAvailable() )
|
|
56
|
+
{
|
|
57
|
+
return {} ;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try
|
|
61
|
+
{
|
|
62
|
+
const raw = window.localStorage.getItem( key ) ;
|
|
63
|
+
|
|
64
|
+
if ( !raw )
|
|
65
|
+
{
|
|
66
|
+
return {} ;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parsed = JSON.parse( raw ) ;
|
|
70
|
+
|
|
71
|
+
if ( !parsed || typeof parsed !== 'object' )
|
|
72
|
+
{
|
|
73
|
+
return {} ;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if ( parsed[ COLLAPSE_STATE_VERSION_KEY ] !== COLLAPSE_STATE_VERSION )
|
|
77
|
+
{
|
|
78
|
+
return {} ;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const items = parsed[ COLLAPSE_STATE_ITEMS_KEY ] ;
|
|
82
|
+
|
|
83
|
+
return ( items && typeof items === 'object' ) ? items : {} ;
|
|
84
|
+
}
|
|
85
|
+
catch
|
|
86
|
+
{
|
|
87
|
+
return {} ;
|
|
88
|
+
}
|
|
89
|
+
} ;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Persists the per-id open/closed map under the given storage key.
|
|
93
|
+
*
|
|
94
|
+
* Silently no-ops when the key is empty, when storage is unavailable, or
|
|
95
|
+
* when the write fails (quota, locked storage, etc.). Never throws.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} [key] - Opaque storage key chosen by the consumer.
|
|
98
|
+
* @param {Record<string, boolean>} [state] - Per-id open/closed map.
|
|
99
|
+
* @returns {boolean} `true` when the write succeeded, `false` otherwise.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```js
|
|
103
|
+
* persistCollapseState( 'my-app:nav:v1' , { lab: true } ) ;
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export const persistCollapseState = ( key , state ) =>
|
|
107
|
+
{
|
|
108
|
+
if ( !notEmpty( key ) || !isStorageAvailable() )
|
|
109
|
+
{
|
|
110
|
+
return false ;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try
|
|
114
|
+
{
|
|
115
|
+
const payload =
|
|
116
|
+
{
|
|
117
|
+
[ COLLAPSE_STATE_VERSION_KEY ] : COLLAPSE_STATE_VERSION ,
|
|
118
|
+
[ COLLAPSE_STATE_ITEMS_KEY ] : state ?? {} ,
|
|
119
|
+
} ;
|
|
120
|
+
|
|
121
|
+
window.localStorage.setItem( key , JSON.stringify( payload ) ) ;
|
|
122
|
+
|
|
123
|
+
return true ;
|
|
124
|
+
}
|
|
125
|
+
catch
|
|
126
|
+
{
|
|
127
|
+
return false ;
|
|
128
|
+
}
|
|
129
|
+
} ;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation collapse persistence constants.
|
|
3
|
+
*
|
|
4
|
+
* @module contexts/navigation/helpers/constants
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default-mode identifiers for collapse open/closed state.
|
|
9
|
+
*
|
|
10
|
+
* - `OPEN` — every collapse starts open (current legacy behaviour).
|
|
11
|
+
* - `CLOSED` — every collapse starts closed.
|
|
12
|
+
* - `AUTO` — open when a descendant matches the current pathname,
|
|
13
|
+
* closed otherwise.
|
|
14
|
+
*
|
|
15
|
+
* @type {Readonly<{ OPEN: 'open', CLOSED: 'closed', AUTO: 'auto' }>}
|
|
16
|
+
*/
|
|
17
|
+
export const COLLAPSE_MODES = Object.freeze
|
|
18
|
+
({
|
|
19
|
+
OPEN : 'open' ,
|
|
20
|
+
CLOSED : 'closed' ,
|
|
21
|
+
AUTO : 'auto' ,
|
|
22
|
+
}) ;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Allowed values for the `defaultMode` prop of `NavigationProvider`.
|
|
26
|
+
*
|
|
27
|
+
* @type {ReadonlyArray<'open' | 'closed' | 'auto'>}
|
|
28
|
+
*/
|
|
29
|
+
export const COLLAPSE_MODE_VALUES = Object.freeze
|
|
30
|
+
([
|
|
31
|
+
COLLAPSE_MODES.OPEN ,
|
|
32
|
+
COLLAPSE_MODES.CLOSED ,
|
|
33
|
+
COLLAPSE_MODES.AUTO ,
|
|
34
|
+
]) ;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default mode used when `NavigationProvider` does not receive an explicit
|
|
38
|
+
* `defaultMode`. Matches the legacy behaviour of `<details open>`.
|
|
39
|
+
*/
|
|
40
|
+
export const DEFAULT_COLLAPSE_MODE = COLLAPSE_MODES.OPEN ;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Schema version of the payload written to `localStorage`.
|
|
44
|
+
* Bump when the on-disk shape changes — older payloads are then ignored
|
|
45
|
+
* silently rather than throwing or surfacing stale state.
|
|
46
|
+
*/
|
|
47
|
+
export const COLLAPSE_STATE_VERSION = 1 ;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Key inside the persisted JSON payload that holds the per-id boolean map.
|
|
51
|
+
* Centralised here so the storage helpers and any future migration code
|
|
52
|
+
* agree on the same string.
|
|
53
|
+
*/
|
|
54
|
+
export const COLLAPSE_STATE_ITEMS_KEY = 'items' ;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Key inside the persisted JSON payload that holds the schema version.
|
|
58
|
+
*/
|
|
59
|
+
export const COLLAPSE_STATE_VERSION_KEY = 'v' ;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive matcher: tells whether a navigation item (typically a
|
|
3
|
+
* `COLLAPSE`) contains a descendant `LINK` whose `path` matches the
|
|
4
|
+
* current pathname.
|
|
5
|
+
*
|
|
6
|
+
* @module contexts/navigation/helpers/containsActivePath
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import notEmpty from 'vegas-js-core/src/strings/notEmpty' ;
|
|
10
|
+
import startsWith from 'vegas-js-core/src/strings/startsWith' ;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Walks the `items` tree under the given navigation node and returns
|
|
14
|
+
* `true` as soon as one descendant has a `path` that matches `pathname`
|
|
15
|
+
* with `startsWith`. The match semantics are intentionally identical to
|
|
16
|
+
* the active-link rule used in `Link.jsx`.
|
|
17
|
+
*
|
|
18
|
+
* Pure, no React, no side effects. Safe on `null`/`undefined`.
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} [item] - Navigation node (collapse or link).
|
|
21
|
+
* @param {Object[]} [item.items] - Children of a collapse node.
|
|
22
|
+
* @param {string} [item.path] - Path of a link node.
|
|
23
|
+
* @param {string} [pathname] - Current pathname (e.g. from `usePathname`).
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```js
|
|
28
|
+
* containsActivePath(
|
|
29
|
+
* { items: [ { path: '/lab/grid' } ] } ,
|
|
30
|
+
* '/lab/grid'
|
|
31
|
+
* ) ; // → true
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
const containsActivePath = ( item , pathname ) =>
|
|
35
|
+
{
|
|
36
|
+
if ( !item || !notEmpty( pathname ) )
|
|
37
|
+
{
|
|
38
|
+
return false ;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if ( notEmpty( item.path ) && startsWith( pathname , item.path ) )
|
|
42
|
+
{
|
|
43
|
+
return true ;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ( !Array.isArray( item.items ) || item.items.length === 0 )
|
|
47
|
+
{
|
|
48
|
+
return false ;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return item.items.some( ( child ) => containsActivePath( child , pathname ) ) ;
|
|
52
|
+
} ;
|
|
53
|
+
|
|
54
|
+
export default containsActivePath ;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds the ids of every collapse whose subtree contains the current
|
|
3
|
+
* pathname.
|
|
4
|
+
*
|
|
5
|
+
* @module contexts/navigation/helpers/findActiveAncestorIds
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import notEmpty from 'vegas-js-core/src/strings/notEmpty' ;
|
|
9
|
+
|
|
10
|
+
import { COLLAPSE } from '../types' ;
|
|
11
|
+
import containsActivePath from './containsActivePath' ;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Walks the navigation tree and returns the id of every `COLLAPSE` node
|
|
15
|
+
* whose subtree contains a `LINK` matching `pathname`. Order is
|
|
16
|
+
* outer-first (root collapses come before their nested ones), which is
|
|
17
|
+
* convenient for callers that want to open ancestors top-down.
|
|
18
|
+
*
|
|
19
|
+
* Pure, no React, no side effects. Safe on `null`/`undefined`.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object[]} [items] - Navigation tree (typically the provider's
|
|
22
|
+
* internal `navigation` array).
|
|
23
|
+
* @param {string} [pathname] - Current pathname.
|
|
24
|
+
* @returns {string[]} Array of collapse ids; empty when nothing matches.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```js
|
|
28
|
+
* findActiveAncestorIds(
|
|
29
|
+
* [{ id: 'lab', type: 'collapse', items: [
|
|
30
|
+
* { id: 'navigation', type: 'collapse', items: [
|
|
31
|
+
* { type: 'link', path: '/lab/menus' }
|
|
32
|
+
* ] }
|
|
33
|
+
* ] }],
|
|
34
|
+
* '/lab/menus'
|
|
35
|
+
* ) ; // → [ 'lab' , 'navigation' ]
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
const findActiveAncestorIds = ( items , pathname ) =>
|
|
39
|
+
{
|
|
40
|
+
const ids = [] ;
|
|
41
|
+
|
|
42
|
+
if ( !Array.isArray( items ) || items.length === 0 || !notEmpty( pathname ) )
|
|
43
|
+
{
|
|
44
|
+
return ids ;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const walk = ( list ) =>
|
|
48
|
+
{
|
|
49
|
+
for ( const item of list )
|
|
50
|
+
{
|
|
51
|
+
if ( !item || item.type !== COLLAPSE )
|
|
52
|
+
{
|
|
53
|
+
continue ;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if ( !containsActivePath( item , pathname ) )
|
|
57
|
+
{
|
|
58
|
+
continue ;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ( notEmpty( item.id ) )
|
|
62
|
+
{
|
|
63
|
+
ids.push( item.id ) ;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if ( Array.isArray( item.items ) && item.items.length > 0 )
|
|
67
|
+
{
|
|
68
|
+
walk( item.items ) ;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} ;
|
|
72
|
+
|
|
73
|
+
walk( items ) ;
|
|
74
|
+
|
|
75
|
+
return ids ;
|
|
76
|
+
} ;
|
|
77
|
+
|
|
78
|
+
export default findActiveAncestorIds ;
|
|
@@ -15,6 +15,8 @@ import mapI18nBadge from './mapI18nBadge'
|
|
|
15
15
|
* @param {React.ComponentType} [item.Icon] - Icon component.
|
|
16
16
|
* @param {Object} [item.badge] - Badge configuration.
|
|
17
17
|
* @param {Object[]} [item.items] - Child items (for collapse type).
|
|
18
|
+
* @param {boolean} [item.defaultOpen] - Per-item override for the
|
|
19
|
+
* collapse open/closed default. Only meaningful when `type === 'collapse'`.
|
|
18
20
|
* @param {Object} locale - Locale data.
|
|
19
21
|
*
|
|
20
22
|
* @returns {Object} Mapped navigation item with localized label.
|
|
@@ -51,7 +53,7 @@ import mapI18nBadge from './mapI18nBadge'
|
|
|
51
53
|
*/
|
|
52
54
|
const mapI18nItem = ( item , locale ) =>
|
|
53
55
|
{
|
|
54
|
-
const { badge , className , Icon , id , items , label , path , type } = item ;
|
|
56
|
+
const { badge , className , defaultOpen , Icon , id , items , label , path , type } = item ;
|
|
55
57
|
|
|
56
58
|
const mappedItems = type === COLLAPSE && items?.length > 0
|
|
57
59
|
? items.map( ( child ) => mapI18nItem( child , locale ) )
|
|
@@ -64,14 +66,15 @@ const mapI18nItem = ( item , locale ) =>
|
|
|
64
66
|
const mappedLabel = locale?.[ id ] ?? label ?? '' ;
|
|
65
67
|
|
|
66
68
|
return {
|
|
67
|
-
badge
|
|
68
|
-
className
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
badge : mappedBadge ,
|
|
70
|
+
className ,
|
|
71
|
+
defaultOpen ,
|
|
72
|
+
Icon ,
|
|
73
|
+
id ,
|
|
74
|
+
items : mappedItems ,
|
|
75
|
+
label : mappedLabel ,
|
|
76
|
+
path ,
|
|
77
|
+
type ,
|
|
75
78
|
} ;
|
|
76
79
|
} ;
|
|
77
80
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure resolver for the open/closed state of a single collapse node.
|
|
3
|
+
*
|
|
4
|
+
* @module contexts/navigation/helpers/resolveCollapseOpen
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import notEmpty from 'vegas-js-core/src/strings/notEmpty' ;
|
|
8
|
+
|
|
9
|
+
import { COLLAPSE_MODES , DEFAULT_COLLAPSE_MODE } from './constants' ;
|
|
10
|
+
import containsActivePath from './containsActivePath' ;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the effective open state for a collapse, in this priority
|
|
14
|
+
* order:
|
|
15
|
+
*
|
|
16
|
+
* 1. Persisted user choice (`persisted[id]`) — explicit win.
|
|
17
|
+
* 2. Auto-mode pathname match — only when `defaultMode === 'auto'`.
|
|
18
|
+
* 3. Per-item `defaultOpen` — author override on the item.
|
|
19
|
+
* 4. Global `defaultMode` — `'open'` truthy, anything else falsy.
|
|
20
|
+
*
|
|
21
|
+
* Pure: no React, no DOM, no storage access. Safe to call during render.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} params
|
|
24
|
+
* @param {string} [params.id] - Item id (used to look up `persisted`).
|
|
25
|
+
* @param {Object} [params.item] - The collapse item itself (read for
|
|
26
|
+
* `defaultOpen` and walked for the auto-mode pathname match).
|
|
27
|
+
* @param {Record<string, boolean>} [params.persisted] - Map loaded from
|
|
28
|
+
* storage; missing keys mean "no user choice yet".
|
|
29
|
+
* @param {string} [params.pathname] - Current pathname. Only consulted
|
|
30
|
+
* in `auto` mode.
|
|
31
|
+
* @param {'open' | 'closed' | 'auto'} [params.defaultMode] - Global
|
|
32
|
+
* provider mode. Defaults to `'open'`.
|
|
33
|
+
* @returns {boolean} Effective open state.
|
|
34
|
+
*/
|
|
35
|
+
const resolveCollapseOpen = (
|
|
36
|
+
{
|
|
37
|
+
id ,
|
|
38
|
+
item ,
|
|
39
|
+
persisted ,
|
|
40
|
+
pathname ,
|
|
41
|
+
defaultMode = DEFAULT_COLLAPSE_MODE ,
|
|
42
|
+
} = {} ) =>
|
|
43
|
+
{
|
|
44
|
+
if ( notEmpty( id ) && persisted && Object.hasOwn( persisted , id ) )
|
|
45
|
+
{
|
|
46
|
+
return Boolean( persisted[ id ] ) ;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if ( defaultMode === COLLAPSE_MODES.AUTO && containsActivePath( item , pathname ) )
|
|
50
|
+
{
|
|
51
|
+
return true ;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if ( item && typeof item.defaultOpen === 'boolean' )
|
|
55
|
+
{
|
|
56
|
+
return item.defaultOpen ;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return defaultMode === COLLAPSE_MODES.OPEN ;
|
|
60
|
+
} ;
|
|
61
|
+
|
|
62
|
+
export default resolveCollapseOpen ;
|