oihana-next-ui 0.2.4 → 0.2.5
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/package.json +1 -1
- package/src/@configs/navigation.js +2 -0
- package/src/@locale/navigation.js +2 -0
- package/src/app/lab/@tabs/tooltips/page.js +29 -0
- package/src/components/Tooltip.jsx +3 -1
- package/src/components/menus/MenuLink.jsx +1 -1
- package/src/components/ranges/Range.jsx +11 -6
- package/src/components/rating/Rating.jsx +1 -1
- package/src/contexts/navigation/context.js +3 -0
- package/src/contexts/navigation/helpers/containsActivePath.js +4 -3
- package/src/contexts/navigation/helpers/findActiveLinkPath.js +69 -0
- package/src/contexts/navigation/helpers/isPathMatch.js +51 -0
- package/src/contexts/navigation/provider.js +28 -12
- package/src/demo/ranges/RangeDemo.jsx +28 -0
- package/src/demo/rating/RatingDemo.jsx +17 -0
- package/src/demo/tooltips/TooltipDemo.jsx +100 -0
- package/src/display/ui/navigation/Link.jsx +12 -2
- package/src/themes/components/range.js +21 -4
- package/src/themes/components/rating.js +39 -12
- package/src/themes/components/tooltip.js +28 -2
- package/src/version.js +1 -1
package/package.json
CHANGED
|
@@ -48,6 +48,7 @@ import { FaSpinner as SpinnerIcon } from "react-icons/fa";
|
|
|
48
48
|
import { GrStatusPlaceholder as StatusIcon } from "react-icons/gr";
|
|
49
49
|
import { BsTextareaResize as TextAreaIcon } from "react-icons/bs" ;
|
|
50
50
|
import { TiMessages as ToastIcon } from "react-icons/ti" ;
|
|
51
|
+
import { LuMessageSquareText as TooltipIcon } from "react-icons/lu";
|
|
51
52
|
import { FaToggleOn as ToggleIcon } from "react-icons/fa";
|
|
52
53
|
|
|
53
54
|
const navigation =
|
|
@@ -127,6 +128,7 @@ const navigation =
|
|
|
127
128
|
{ id : 'radialProgress' , type : LINK , Icon : RadialProgressIcon , path : '/lab/radialProgress' } ,
|
|
128
129
|
{ id : 'spinners' , type : LINK , Icon : SpinnerIcon , path : '/lab/spinners' } ,
|
|
129
130
|
{ id : 'toasts' , type : LINK , Icon : ToastIcon , path : '/lab/toasts' } ,
|
|
131
|
+
{ id : 'tooltips' , type : LINK , Icon : TooltipIcon , path : '/lab/tooltips' } ,
|
|
130
132
|
]
|
|
131
133
|
} ,
|
|
132
134
|
{
|
|
@@ -34,6 +34,7 @@ const navigation =
|
|
|
34
34
|
radialProgress : 'Radial Progress' ,
|
|
35
35
|
spinners : 'Spinners' ,
|
|
36
36
|
toasts : 'Toasts' ,
|
|
37
|
+
tooltips : 'Tooltips' ,
|
|
37
38
|
form : 'Formulaire' ,
|
|
38
39
|
checkboxes : 'Cases à cocher' ,
|
|
39
40
|
inputs : 'Champs de saisie' ,
|
|
@@ -85,6 +86,7 @@ const navigation =
|
|
|
85
86
|
radialProgress : 'Radial Progress' ,
|
|
86
87
|
spinners : 'Spinners' ,
|
|
87
88
|
toasts : 'Toasts' ,
|
|
89
|
+
tooltips : 'Tooltips' ,
|
|
88
90
|
form : 'Form' ,
|
|
89
91
|
checkboxes : 'Checkboxes' ,
|
|
90
92
|
inputs : 'Inputs' ,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client' ;
|
|
2
|
+
|
|
3
|
+
import TooltipDemo from '@/demo/tooltips/TooltipDemo' ;
|
|
4
|
+
import Container from '@/display/Container' ;
|
|
5
|
+
import Page from '@/display/Page' ;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tooltip showcase page.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} props
|
|
11
|
+
*/
|
|
12
|
+
const TooltipsShowcase = () =>
|
|
13
|
+
{
|
|
14
|
+
return (
|
|
15
|
+
<Page full className='gap-8'>
|
|
16
|
+
|
|
17
|
+
<Container className="text-center" maxWidth="max-w-4xl">
|
|
18
|
+
<h1 className="text-4xl font-bold bg-linear-to-r from-secondary to-primary inline-block text-transparent bg-clip-text">
|
|
19
|
+
Tooltip Component
|
|
20
|
+
</h1>
|
|
21
|
+
</Container>
|
|
22
|
+
|
|
23
|
+
<TooltipDemo />
|
|
24
|
+
|
|
25
|
+
</Page>
|
|
26
|
+
) ;
|
|
27
|
+
} ;
|
|
28
|
+
|
|
29
|
+
export default TooltipsShowcase ;
|
|
@@ -45,6 +45,7 @@ import getTooltipClassNames from '../themes/components/tooltip' ;
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* @param {Object} props
|
|
48
|
+
* @param {import('../themes/components/tooltip').TooltipAlignment} [props.align] - Tooltip alignment ('start' | 'center' | 'end').
|
|
48
49
|
* @param {React.ElementType} [props.as] - Root element type.
|
|
49
50
|
* @param {React.ReactNode} [props.children] - Tooltip trigger content.
|
|
50
51
|
* @param {string} [props.className] - Additional class name.
|
|
@@ -57,6 +58,7 @@ import getTooltipClassNames from '../themes/components/tooltip' ;
|
|
|
57
58
|
*/
|
|
58
59
|
const Tooltip =
|
|
59
60
|
({
|
|
61
|
+
align ,
|
|
60
62
|
as ,
|
|
61
63
|
children ,
|
|
62
64
|
className ,
|
|
@@ -73,7 +75,7 @@ const Tooltip =
|
|
|
73
75
|
|
|
74
76
|
const Component = as || 'div' ;
|
|
75
77
|
|
|
76
|
-
const classNames = getTooltipClassNames({ className , color , open , position }) ;
|
|
78
|
+
const classNames = getTooltipClassNames({ align , className , color , open , position }) ;
|
|
77
79
|
|
|
78
80
|
return (
|
|
79
81
|
<Component
|
|
@@ -13,6 +13,8 @@ const Range =
|
|
|
13
13
|
error,
|
|
14
14
|
color,
|
|
15
15
|
size = 'md',
|
|
16
|
+
orientation = 'horizontal',
|
|
17
|
+
height = 'h-64',
|
|
16
18
|
min = 0,
|
|
17
19
|
max = 100,
|
|
18
20
|
step = 1,
|
|
@@ -33,6 +35,8 @@ const Range =
|
|
|
33
35
|
...rangeProps
|
|
34
36
|
}) =>
|
|
35
37
|
{
|
|
38
|
+
const isVertical = orientation === 'vertical' ;
|
|
39
|
+
|
|
36
40
|
const isControlled = controlledValue !== undefined ;
|
|
37
41
|
const [ internalValue, setInternalValue ] = useState( defaultValue ?? min ) ;
|
|
38
42
|
|
|
@@ -54,12 +58,13 @@ const Range =
|
|
|
54
58
|
|
|
55
59
|
const rangeClasses = getRangeClasses({
|
|
56
60
|
color: hasError ? 'error' : color,
|
|
61
|
+
orientation,
|
|
57
62
|
size,
|
|
58
63
|
className: rangeClassName,
|
|
59
64
|
}) ;
|
|
60
65
|
|
|
61
|
-
// Generate markers
|
|
62
|
-
const markers = showMarkers ? (() =>
|
|
66
|
+
// Generate markers (horizontal only — vertical markers are not supported yet)
|
|
67
|
+
const markers = ( showMarkers && !isVertical ) ? (() =>
|
|
63
68
|
{
|
|
64
69
|
const count = Math.floor(( max - min ) / step ) + 1 ;
|
|
65
70
|
const items = [] ;
|
|
@@ -74,8 +79,8 @@ const Range =
|
|
|
74
79
|
return items ;
|
|
75
80
|
})() : null ;
|
|
76
81
|
|
|
77
|
-
// Generate marker labels
|
|
78
|
-
const labels = markerLabels ? markerLabels.map(( label, i ) => (
|
|
82
|
+
// Generate marker labels (horizontal only)
|
|
83
|
+
const labels = ( markerLabels && !isVertical ) ? markerLabels.map(( label, i ) => (
|
|
79
84
|
<span key={ i } className="text-xs">{ label }</span>
|
|
80
85
|
)) : null ;
|
|
81
86
|
|
|
@@ -119,12 +124,12 @@ const Range =
|
|
|
119
124
|
</span>
|
|
120
125
|
)}
|
|
121
126
|
|
|
122
|
-
<div className=
|
|
127
|
+
<div className={ cn( isVertical ? height : 'flex-1 max-w-full' ) }>
|
|
123
128
|
<input
|
|
124
129
|
type="range"
|
|
125
130
|
id={ id }
|
|
126
131
|
name={ name }
|
|
127
|
-
className={ cn( rangeClasses, 'w-full' ) }
|
|
132
|
+
className={ cn( rangeClasses, !isVertical && 'w-full' ) }
|
|
128
133
|
min={ min }
|
|
129
134
|
max={ max }
|
|
130
135
|
step={ step }
|
|
@@ -41,7 +41,7 @@ import cn from '../../themes/helpers/cn' ;
|
|
|
41
41
|
* @param {number} [props.defaultValue] - Default value for uncontrolled mode
|
|
42
42
|
* @param {Function} [props.onChange] - Change handler (value) => void
|
|
43
43
|
* @param {number} [props.count=5] - Number of stars/items
|
|
44
|
-
* @param {import('../../themes/components/rating').RatingSize} [props.size='md'] - Size: 'xs'
|
|
44
|
+
* @param {import('../../themes/components/rating').RatingSize | import('../../themes/components/rating').ResponsiveRatingSize} [props.size='md'] - Size: scalar ('xs'…'xl') or responsive object (e.g. `{ xs: 'sm', md: 'lg' }`)
|
|
45
45
|
* @param {import('../../themes/components/mask').MaskShape} [props.shape='star-2'] - Mask shape
|
|
46
46
|
* @param {import('../../themes/colors/backgroundColor').BackgroundColorValue} [props.color] - Background color
|
|
47
47
|
* @param {boolean} [props.half=false] - Enable half-star ratings
|
|
@@ -17,6 +17,9 @@ import { createContext } from 'react' ;
|
|
|
17
17
|
* priority chain: persisted → auto(pathname) → item.defaultOpen → defaultMode.
|
|
18
18
|
* @property {string | null} pathname - Current pathname, captured by the
|
|
19
19
|
* provider so consumers (e.g. `Collapse`) don't have to read it again.
|
|
20
|
+
* @property {string | null} activePath - Longest LINK path matching the
|
|
21
|
+
* current route (the single "winning" link). `null` when nothing
|
|
22
|
+
* matches. Consumed by `Link` to decide its active state.
|
|
20
23
|
*/
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* @module contexts/navigation/helpers/containsActivePath
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import notEmpty
|
|
10
|
-
|
|
9
|
+
import notEmpty from 'vegas-js-core/src/strings/notEmpty' ;
|
|
10
|
+
|
|
11
|
+
import isPathMatch from './isPathMatch' ;
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Walks the `items` tree under the given navigation node and returns
|
|
@@ -38,7 +39,7 @@ const containsActivePath = ( item , pathname ) =>
|
|
|
38
39
|
return false ;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
if ( notEmpty( item.path ) &&
|
|
42
|
+
if ( notEmpty( item.path ) && isPathMatch( pathname , item.path ) )
|
|
42
43
|
{
|
|
43
44
|
return true ;
|
|
44
45
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the single "winning" link path for the current pathname :
|
|
3
|
+
* among every `LINK` whose `path` matches (segment-aware), the LONGEST
|
|
4
|
+
* one wins. This is what disambiguates a destination nested under another
|
|
5
|
+
* — `/me/customers` wins over `/me`, so only « Mes clients » lights up,
|
|
6
|
+
* not « Mon profil ». A profile sub-page with no dedicated link
|
|
7
|
+
* (`/me/sessions`) has only `/me` as a match, so `/me` stays the winner.
|
|
8
|
+
*
|
|
9
|
+
* Pure, no React. Walks `COLLAPSE` children too (their own `path` is
|
|
10
|
+
* undefined and simply skipped). Returns `null` when nothing matches.
|
|
11
|
+
*
|
|
12
|
+
* @module contexts/navigation/helpers/findActiveLinkPath
|
|
13
|
+
*
|
|
14
|
+
* @param {Object[]} [items] - Navigation tree (the provider's internal array).
|
|
15
|
+
* @param {string} [pathname] - Current pathname.
|
|
16
|
+
* @returns {string|null} The longest matching link path, or `null`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```js
|
|
20
|
+
* findActiveLinkPath(
|
|
21
|
+
* [ { path: '/me' } , { path: '/me/customers' } ] ,
|
|
22
|
+
* '/me/customers/137'
|
|
23
|
+
* ) ; // → '/me/customers'
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import notEmpty from 'vegas-js-core/src/strings/notEmpty' ;
|
|
28
|
+
|
|
29
|
+
import isPathMatch from './isPathMatch' ;
|
|
30
|
+
|
|
31
|
+
const findActiveLinkPath = ( items , pathname ) =>
|
|
32
|
+
{
|
|
33
|
+
if ( !Array.isArray( items ) || items.length === 0 || !notEmpty( pathname ) )
|
|
34
|
+
{
|
|
35
|
+
return null ;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let best = null ;
|
|
39
|
+
|
|
40
|
+
const walk = list =>
|
|
41
|
+
{
|
|
42
|
+
for ( const item of list )
|
|
43
|
+
{
|
|
44
|
+
if ( !item )
|
|
45
|
+
{
|
|
46
|
+
continue ;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if ( notEmpty( item.path ) && isPathMatch( pathname , item.path ) )
|
|
50
|
+
{
|
|
51
|
+
if ( best === null || item.path.length > best.length )
|
|
52
|
+
{
|
|
53
|
+
best = item.path ;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if ( Array.isArray( item.items ) && item.items.length > 0 )
|
|
58
|
+
{
|
|
59
|
+
walk( item.items ) ;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} ;
|
|
63
|
+
|
|
64
|
+
walk( items ) ;
|
|
65
|
+
|
|
66
|
+
return best ;
|
|
67
|
+
} ;
|
|
68
|
+
|
|
69
|
+
export default findActiveLinkPath ;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment-aware path matcher : tells whether a navigation `path` covers
|
|
3
|
+
* the current `pathname`.
|
|
4
|
+
*
|
|
5
|
+
* A path matches when the pathname is exactly it, or a descendant of it
|
|
6
|
+
* on a segment boundary — so `/me` matches `/me` and `/me/sessions` but
|
|
7
|
+
* NOT `/menu`. The root `'/'` only ever matches `'/'` (it would otherwise
|
|
8
|
+
* shadow every route).
|
|
9
|
+
*
|
|
10
|
+
* Single source of truth shared by `findActiveLinkPath` (link highlight)
|
|
11
|
+
* and `containsActivePath` (collapse open / active-ancestor), so both
|
|
12
|
+
* surfaces agree on what "active" means.
|
|
13
|
+
*
|
|
14
|
+
* @module contexts/navigation/helpers/isPathMatch
|
|
15
|
+
*
|
|
16
|
+
* @param {string} [pathname] - Current pathname (e.g. from `usePathname`).
|
|
17
|
+
* @param {string} [path] - A navigation link `path`.
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```js
|
|
22
|
+
* isPathMatch( '/me/sessions' , '/me' ) ; // → true
|
|
23
|
+
* isPathMatch( '/me/customers' , '/me' ) ; // → true (use longest-match to disambiguate)
|
|
24
|
+
* isPathMatch( '/menu' , '/me' ) ; // → false (segment boundary)
|
|
25
|
+
* isPathMatch( '/anything' , '/' ) ; // → false (root matches only '/')
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import notEmpty from 'vegas-js-core/src/strings/notEmpty' ;
|
|
30
|
+
|
|
31
|
+
const isPathMatch = ( pathname , path ) =>
|
|
32
|
+
{
|
|
33
|
+
if ( !notEmpty( pathname ) || !notEmpty( path ) )
|
|
34
|
+
{
|
|
35
|
+
return false ;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if ( pathname === path )
|
|
39
|
+
{
|
|
40
|
+
return true ;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ( path === '/' )
|
|
44
|
+
{
|
|
45
|
+
return false ;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return pathname.startsWith( path + '/' ) ;
|
|
49
|
+
} ;
|
|
50
|
+
|
|
51
|
+
export default isPathMatch ;
|
|
@@ -6,18 +6,24 @@ import { usePathname } from 'next/navigation' ;
|
|
|
6
6
|
|
|
7
7
|
import useI18n from '../locale/useI18n' ;
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import
|
|
10
|
+
{
|
|
10
11
|
loadCollapseState ,
|
|
11
12
|
persistCollapseState ,
|
|
12
|
-
}
|
|
13
|
+
}
|
|
14
|
+
from './helpers/collapseStorage' ;
|
|
15
|
+
|
|
13
16
|
import {
|
|
14
|
-
COLLAPSE_MODES
|
|
15
|
-
COLLAPSE_MODE_VALUES
|
|
17
|
+
COLLAPSE_MODES ,
|
|
18
|
+
COLLAPSE_MODE_VALUES ,
|
|
16
19
|
DEFAULT_COLLAPSE_MODE ,
|
|
17
|
-
}
|
|
20
|
+
}
|
|
21
|
+
from './helpers/constants' ;
|
|
22
|
+
|
|
18
23
|
import findActiveAncestorIds from './helpers/findActiveAncestorIds' ;
|
|
19
|
-
import
|
|
20
|
-
import
|
|
24
|
+
import findActiveLinkPath from './helpers/findActiveLinkPath' ;
|
|
25
|
+
import mapI18nItem from './helpers/mapI18nItem' ;
|
|
26
|
+
import resolveCollapseOpen from './helpers/resolveCollapseOpen' ;
|
|
21
27
|
|
|
22
28
|
import NavigationContext from './context' ;
|
|
23
29
|
|
|
@@ -72,6 +78,15 @@ const NavigationProvider =
|
|
|
72
78
|
? _navigation.map( ( item ) => mapI18nItem( item , locale ) )
|
|
73
79
|
: null ;
|
|
74
80
|
|
|
81
|
+
// The single active link path: longest LINK path matching the
|
|
82
|
+
// current route. Drives the active-link highlight (see Link.jsx) so
|
|
83
|
+
// a nested destination (/me/customers) deactivates its parent (/me).
|
|
84
|
+
const activePath = useMemo
|
|
85
|
+
(
|
|
86
|
+
() => findActiveLinkPath( _navigation , pathname ) ,
|
|
87
|
+
[ _navigation , pathname ] ,
|
|
88
|
+
) ;
|
|
89
|
+
|
|
75
90
|
// Collapse state is initialised empty so the first server render is
|
|
76
91
|
// deterministic (no localStorage on the server). Hydration happens in
|
|
77
92
|
// the effect below, after mount, which never causes a hydration
|
|
@@ -188,15 +203,16 @@ const NavigationProvider =
|
|
|
188
203
|
|
|
189
204
|
const value = useMemo( () =>
|
|
190
205
|
({
|
|
191
|
-
|
|
192
|
-
setNavigation ,
|
|
193
|
-
defaultMode ,
|
|
206
|
+
activePath ,
|
|
194
207
|
collapses ,
|
|
195
|
-
|
|
208
|
+
defaultMode ,
|
|
196
209
|
getCollapseOpen ,
|
|
210
|
+
navigation ,
|
|
197
211
|
pathname ,
|
|
212
|
+
setNavigation ,
|
|
213
|
+
setCollapse ,
|
|
198
214
|
})
|
|
199
|
-
, [
|
|
215
|
+
, [ activePath , defaultMode , collapses , getCollapseOpen , navigation , pathname , setCollapse ] ) ;
|
|
200
216
|
|
|
201
217
|
return (
|
|
202
218
|
<NavigationContext value={ value }>
|
|
@@ -452,6 +452,34 @@ const RangeDemo = () =>
|
|
|
452
452
|
</div>
|
|
453
453
|
</div>
|
|
454
454
|
|
|
455
|
+
<Divider />
|
|
456
|
+
|
|
457
|
+
{/* Vertical (daisyUI 5.6) */}
|
|
458
|
+
<div className="flex flex-col gap-4">
|
|
459
|
+
<h3 className="text-xl font-semibold">Vertical</h3>
|
|
460
|
+
<p className="text-sm text-base-content/70">
|
|
461
|
+
<code>orientation="vertical"</code> — height adjustable via the <code>height</code> prop
|
|
462
|
+
(default <code>h-64</code>). Markers are not rendered in vertical mode.
|
|
463
|
+
</p>
|
|
464
|
+
|
|
465
|
+
<div className="flex items-end gap-10">
|
|
466
|
+
<Range orientation="vertical" defaultValue={ 40 } />
|
|
467
|
+
<Range orientation="vertical" defaultValue={ 60 } color="primary" />
|
|
468
|
+
<Range orientation="vertical" defaultValue={ 30 } color="secondary" size="lg" />
|
|
469
|
+
<Range orientation="vertical" defaultValue={ 80 } color="accent" size="sm" height="h-40" />
|
|
470
|
+
|
|
471
|
+
<Range
|
|
472
|
+
label="Volume"
|
|
473
|
+
orientation="vertical"
|
|
474
|
+
defaultValue={ 55 }
|
|
475
|
+
color="success"
|
|
476
|
+
showValue
|
|
477
|
+
valuePosition="bottom"
|
|
478
|
+
formatValue={ (v) => `${v}%` }
|
|
479
|
+
/>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
455
483
|
</Container>
|
|
456
484
|
) ;
|
|
457
485
|
} ;
|
|
@@ -673,6 +673,23 @@ const RatingDemo = () =>
|
|
|
673
673
|
</div>
|
|
674
674
|
</div>
|
|
675
675
|
|
|
676
|
+
<Divider />
|
|
677
|
+
|
|
678
|
+
{/* Responsive size (daisyUI 5.6) */}
|
|
679
|
+
<div className="flex flex-col gap-4">
|
|
680
|
+
<h3 className="text-xl font-semibold">Responsive size</h3>
|
|
681
|
+
<p className="text-sm text-base-content/70">
|
|
682
|
+
<code>size</code> accepts a breakpoint→size object. Resize the window:
|
|
683
|
+
this rating grows <code>sm → md → lg → xl</code> across breakpoints.
|
|
684
|
+
</p>
|
|
685
|
+
<Rating
|
|
686
|
+
name="rating-responsive"
|
|
687
|
+
defaultValue={ 3 }
|
|
688
|
+
color="warning"
|
|
689
|
+
size={ { xs: 'sm', md: 'md', lg: 'lg', xl: 'xl' } }
|
|
690
|
+
/>
|
|
691
|
+
</div>
|
|
692
|
+
|
|
676
693
|
</Container>
|
|
677
694
|
) ;
|
|
678
695
|
} ;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client' ;
|
|
2
|
+
|
|
3
|
+
import Container from '@/display/Container' ;
|
|
4
|
+
import Divider from '@/components/Divider' ;
|
|
5
|
+
import Tooltip from '@/components/Tooltip' ;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tooltip showcase — positions, the new start/center/end alignments, colours,
|
|
9
|
+
* forced-open state and rich content.
|
|
10
|
+
*/
|
|
11
|
+
const TooltipDemo = () =>
|
|
12
|
+
{
|
|
13
|
+
return (
|
|
14
|
+
<Container className="flex flex-col gap-8 bg-base-200/60 p-8 rounded-box" maxWidth="max-w-7xl">
|
|
15
|
+
|
|
16
|
+
<h2 className="text-3xl font-bold">Tooltip</h2>
|
|
17
|
+
|
|
18
|
+
{/* Positions */}
|
|
19
|
+
<div className="flex flex-col gap-4">
|
|
20
|
+
<h3 className="text-xl font-semibold">Positions</h3>
|
|
21
|
+
<div className="flex flex-wrap items-center gap-6 p-6">
|
|
22
|
+
<Tooltip tip="Top" position="top"><button className="btn">Top</button></Tooltip>
|
|
23
|
+
<Tooltip tip="Bottom" position="bottom"><button className="btn">Bottom</button></Tooltip>
|
|
24
|
+
<Tooltip tip="Left" position="left"><button className="btn">Left</button></Tooltip>
|
|
25
|
+
<Tooltip tip="Right" position="right"><button className="btn">Right</button></Tooltip>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<Divider />
|
|
30
|
+
|
|
31
|
+
{/* Alignments (new in 5.6) */}
|
|
32
|
+
<div className="flex flex-col gap-4">
|
|
33
|
+
<h3 className="text-xl font-semibold">Alignments (start / center / end)</h3>
|
|
34
|
+
<p className="text-sm text-base-content/70">
|
|
35
|
+
Independent from the position. Shown forced-open on a wide trigger so the offset is visible.
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<div className="flex flex-col gap-12 pt-16 pb-4">
|
|
39
|
+
<div className="flex flex-wrap gap-12">
|
|
40
|
+
<Tooltip tip="Aligned to start" position="top" align="start" color="primary" open>
|
|
41
|
+
<button className="btn w-64">top · start</button>
|
|
42
|
+
</Tooltip>
|
|
43
|
+
<Tooltip tip="Centered" position="top" align="center" color="primary" open>
|
|
44
|
+
<button className="btn w-64">top · center</button>
|
|
45
|
+
</Tooltip>
|
|
46
|
+
<Tooltip tip="Aligned to end" position="top" align="end" color="primary" open>
|
|
47
|
+
<button className="btn w-64">top · end</button>
|
|
48
|
+
</Tooltip>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div className="flex flex-col gap-12 pb-16 pt-4">
|
|
53
|
+
<div className="flex flex-wrap gap-12">
|
|
54
|
+
<Tooltip tip="Aligned to start" position="bottom" align="start" color="secondary" open>
|
|
55
|
+
<button className="btn w-64">bottom · start</button>
|
|
56
|
+
</Tooltip>
|
|
57
|
+
<Tooltip tip="Centered" position="bottom" align="center" color="secondary" open>
|
|
58
|
+
<button className="btn w-64">bottom · center</button>
|
|
59
|
+
</Tooltip>
|
|
60
|
+
<Tooltip tip="Aligned to end" position="bottom" align="end" color="secondary" open>
|
|
61
|
+
<button className="btn w-64">bottom · end</button>
|
|
62
|
+
</Tooltip>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<Divider />
|
|
68
|
+
|
|
69
|
+
{/* Colours */}
|
|
70
|
+
<div className="flex flex-col gap-4">
|
|
71
|
+
<h3 className="text-xl font-semibold">Colours</h3>
|
|
72
|
+
<div className="flex flex-wrap items-center gap-6 p-6">
|
|
73
|
+
{ [ 'primary' , 'secondary' , 'accent' , 'info' , 'success' , 'warning' , 'error' ].map( ( color ) => (
|
|
74
|
+
<Tooltip key={ color } tip={ color } color={ color } position="top">
|
|
75
|
+
<button className="btn">{ color }</button>
|
|
76
|
+
</Tooltip>
|
|
77
|
+
) ) }
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<Divider />
|
|
82
|
+
|
|
83
|
+
{/* Rich content */}
|
|
84
|
+
<div className="flex flex-col gap-4">
|
|
85
|
+
<h3 className="text-xl font-semibold">Rich content</h3>
|
|
86
|
+
<div className="flex flex-wrap items-center gap-6 p-6">
|
|
87
|
+
<Tooltip position="top" color="neutral">
|
|
88
|
+
<button className="btn">Hover me</button>
|
|
89
|
+
<div className="tooltip-content">
|
|
90
|
+
<p className="text-sm">Rich <strong>HTML</strong> content</p>
|
|
91
|
+
</div>
|
|
92
|
+
</Tooltip>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
</Container>
|
|
97
|
+
) ;
|
|
98
|
+
} ;
|
|
99
|
+
|
|
100
|
+
export default TooltipDemo ;
|
|
@@ -4,18 +4,22 @@
|
|
|
4
4
|
* @module components/menu/Link
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { use } from 'react' ;
|
|
8
|
+
|
|
7
9
|
import NextLink from 'next/link' ;
|
|
8
10
|
|
|
9
11
|
import { usePathname } from 'next/navigation' ;
|
|
10
12
|
|
|
11
13
|
import isObject from 'vegas-js-core/src/isPlainObject' ;
|
|
12
14
|
import notEmpty from 'vegas-js-core/src/strings/notEmpty' ;
|
|
13
|
-
import startsWith from 'vegas-js-core/src/strings/startsWith' ;
|
|
14
15
|
|
|
15
16
|
import cn from '../../../themes/helpers/cn' ;
|
|
16
17
|
|
|
17
18
|
import Badge from '../../../components/Badge' ;
|
|
18
19
|
|
|
20
|
+
import NavigationContext from '../../../contexts/navigation/context' ;
|
|
21
|
+
import isPathMatch from '../../../contexts/navigation/helpers/isPathMatch' ;
|
|
22
|
+
|
|
19
23
|
/**
|
|
20
24
|
* Returns a Badge element from a string or object definition.
|
|
21
25
|
*
|
|
@@ -106,7 +110,13 @@ const Link =
|
|
|
106
110
|
{
|
|
107
111
|
const pathname = usePathname() ;
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
// Defensive read: a Link may be rendered without a NavigationProvider
|
|
114
|
+
// (legacy / standalone). With a provider, the single winning path is
|
|
115
|
+
// pre-computed (longest match) → exact equality. Without, fall back to
|
|
116
|
+
// a local segment-aware match so a lone Link still highlights itself.
|
|
117
|
+
const navigation = use( NavigationContext ) ;
|
|
118
|
+
|
|
119
|
+
const active = navigation ? path === navigation.activePath : isPathMatch( pathname , path ) ;
|
|
110
120
|
|
|
111
121
|
const classNames = cn
|
|
112
122
|
(
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
WARNING,
|
|
21
21
|
} from '../colors' ;
|
|
22
22
|
|
|
23
|
+
import { HORIZONTAL, VERTICAL } from '../enums/orientations' ;
|
|
24
|
+
|
|
23
25
|
/**
|
|
24
26
|
* Valid range colors.
|
|
25
27
|
* @type {string[]}
|
|
@@ -70,6 +72,18 @@ const sizeMap =
|
|
|
70
72
|
[ XS ] : 'range-xs',
|
|
71
73
|
} ;
|
|
72
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Valid range orientations.
|
|
77
|
+
* @type {string[]}
|
|
78
|
+
*/
|
|
79
|
+
export const orientations = [ HORIZONTAL, VERTICAL ] ;
|
|
80
|
+
|
|
81
|
+
// Horizontal is the default (no modifier class) ; only vertical adds one.
|
|
82
|
+
const orientationMap =
|
|
83
|
+
{
|
|
84
|
+
[ VERTICAL ] : 'range-vertical',
|
|
85
|
+
} ;
|
|
86
|
+
|
|
73
87
|
export const RANGE = 'range' ;
|
|
74
88
|
|
|
75
89
|
/**
|
|
@@ -81,6 +95,7 @@ export const RANGE = 'range' ;
|
|
|
81
95
|
* @param {string} [props.beforeClassName] - CSS string prepended.
|
|
82
96
|
* @param {string} [props.className] - CSS string appended.
|
|
83
97
|
* @param {string} [props.color] - Range color variant.
|
|
98
|
+
* @param {string} [props.orientation='horizontal'] - Range orientation ('horizontal' | 'vertical').
|
|
84
99
|
* @param {string} [props.size='md'] - Range size (xs, sm, md, lg, xl).
|
|
85
100
|
*
|
|
86
101
|
* @returns {string} Combined class names.
|
|
@@ -90,8 +105,8 @@ export const RANGE = 'range' ;
|
|
|
90
105
|
* getRangeClasses({ color: 'primary', size: 'lg' }) ;
|
|
91
106
|
* // → 'range range-primary range-lg'
|
|
92
107
|
*
|
|
93
|
-
* getRangeClasses({ color: 'success' }) ;
|
|
94
|
-
* // → 'range range-success range-md'
|
|
108
|
+
* getRangeClasses({ orientation: 'vertical', color: 'success' }) ;
|
|
109
|
+
* // → 'range range-vertical range-success range-md'
|
|
95
110
|
* ```
|
|
96
111
|
*/
|
|
97
112
|
export const getRangeClasses =
|
|
@@ -101,6 +116,7 @@ export const getRangeClasses =
|
|
|
101
116
|
beforeClassName,
|
|
102
117
|
className,
|
|
103
118
|
color,
|
|
119
|
+
orientation = HORIZONTAL,
|
|
104
120
|
size = MD,
|
|
105
121
|
}
|
|
106
122
|
= {} ) => cn
|
|
@@ -111,8 +127,9 @@ export const getRangeClasses =
|
|
|
111
127
|
|
|
112
128
|
[ RANGE ] : true,
|
|
113
129
|
|
|
114
|
-
...!!
|
|
115
|
-
...!!
|
|
130
|
+
...!!orientationMap[orientation] && { [ orientationMap[orientation] ] : true },
|
|
131
|
+
...!!colorMap[color] && { [ colorMap[color] ] : true },
|
|
132
|
+
...!!sizeMap[size] && { [ sizeMap[size] ] : true },
|
|
116
133
|
|
|
117
134
|
...after,
|
|
118
135
|
},
|
|
@@ -3,15 +3,35 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module themes/components/rating
|
|
5
5
|
* @see https://daisyui.com/components/rating
|
|
6
|
+
*
|
|
7
|
+
* @safelist
|
|
8
|
+
* ## Sizes (responsive — daisyUI 5.6)
|
|
9
|
+
* - rating-xs | rating-sm | rating-md | rating-lg | rating-xl
|
|
10
|
+
* - sm:rating-xs | sm:rating-sm | sm:rating-md | sm:rating-lg | sm:rating-xl
|
|
11
|
+
* - md:rating-xs | md:rating-sm | md:rating-md | md:rating-lg | md:rating-xl
|
|
12
|
+
* - lg:rating-xs | lg:rating-sm | lg:rating-md | lg:rating-lg | lg:rating-xl
|
|
13
|
+
* - xl:rating-xs | xl:rating-sm | xl:rating-md | xl:rating-lg | xl:rating-xl
|
|
14
|
+
* - 2xl:rating-xs | 2xl:rating-sm | 2xl:rating-md | 2xl:rating-lg | 2xl:rating-xl
|
|
6
15
|
*/
|
|
7
16
|
|
|
8
17
|
import cn from '../helpers/cn' ;
|
|
18
|
+
|
|
19
|
+
import getResponsiveDefinition , { create } from '../helpers/getResponsiveDefinition' ;
|
|
20
|
+
|
|
9
21
|
import { LG, MD, SM, XL, XS } from '../sizing/sizes' ;
|
|
10
22
|
|
|
11
23
|
// Sizes
|
|
12
24
|
|
|
13
25
|
/**
|
|
14
26
|
* @typedef {'xs' | 'sm' | 'md' | 'lg' | 'xl'} RatingSize
|
|
27
|
+
*
|
|
28
|
+
* @typedef {Object} ResponsiveRatingSize
|
|
29
|
+
* @property {RatingSize} [xs] - Default size (no breakpoint prefix).
|
|
30
|
+
* @property {RatingSize} [sm]
|
|
31
|
+
* @property {RatingSize} [md]
|
|
32
|
+
* @property {RatingSize} [lg]
|
|
33
|
+
* @property {RatingSize} [xl]
|
|
34
|
+
* @property {RatingSize} [xxl]
|
|
15
35
|
*/
|
|
16
36
|
|
|
17
37
|
/**
|
|
@@ -20,14 +40,18 @@ import { LG, MD, SM, XL, XS } from '../sizing/sizes' ;
|
|
|
20
40
|
*/
|
|
21
41
|
export const sizes = [ XS, SM, MD, LG, XL ] ;
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Generates responsive rating size classes (daisyUI 5.6).
|
|
45
|
+
*
|
|
46
|
+
* Accepts a scalar size or a breakpoint→size object ; `xs` is the prefix-less
|
|
47
|
+
* default. Responsive classes are built at runtime, hence the `@safelist` above.
|
|
48
|
+
*
|
|
49
|
+
* @type {Function}
|
|
50
|
+
*/
|
|
51
|
+
export const getRatingSize = getResponsiveDefinition(
|
|
52
|
+
create( 'rating-' ) ,
|
|
53
|
+
value => sizes.includes( value )
|
|
54
|
+
) ;
|
|
31
55
|
|
|
32
56
|
export const RATING = 'rating' ;
|
|
33
57
|
export const RATING_HALF = 'rating-half' ;
|
|
@@ -42,7 +66,7 @@ export const RATING_HIDDEN = 'rating-hidden' ;
|
|
|
42
66
|
* @param {string} [props.beforeClassName] - ClassName to prepend.
|
|
43
67
|
* @param {string} [props.className] - ClassName to append.
|
|
44
68
|
* @param {boolean} [props.half=false] - Enable half-star ratings.
|
|
45
|
-
* @param {RatingSize} [props.size='md'] - Rating size.
|
|
69
|
+
* @param {RatingSize | ResponsiveRatingSize} [props.size='md'] - Rating size (scalar or responsive object).
|
|
46
70
|
*
|
|
47
71
|
* @returns {string} The rating className expression.
|
|
48
72
|
*
|
|
@@ -54,6 +78,9 @@ export const RATING_HIDDEN = 'rating-hidden' ;
|
|
|
54
78
|
* getRatingClasses({ size: 'lg' }) ;
|
|
55
79
|
* // → 'rating rating-lg'
|
|
56
80
|
*
|
|
81
|
+
* getRatingClasses({ size: { xs: 'sm', md: 'lg', xl: 'xl' } }) ;
|
|
82
|
+
* // → 'rating rating-sm md:rating-lg xl:rating-xl'
|
|
83
|
+
*
|
|
57
84
|
* getRatingClasses({ half: true, size: 'xl' }) ;
|
|
58
85
|
* // → 'rating rating-half rating-xl'
|
|
59
86
|
* ```
|
|
@@ -73,12 +100,12 @@ const getRatingClasses =
|
|
|
73
100
|
{
|
|
74
101
|
...before,
|
|
75
102
|
|
|
76
|
-
...half === true
|
|
77
|
-
...!!
|
|
103
|
+
...half === true && { [ RATING_HALF ] : true } ,
|
|
104
|
+
...!!size && getRatingSize( size ) ,
|
|
78
105
|
|
|
79
106
|
...after,
|
|
80
107
|
},
|
|
81
108
|
className,
|
|
82
109
|
) ;
|
|
83
110
|
|
|
84
|
-
export default getRatingClasses ;
|
|
111
|
+
export default getRatingClasses ;
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
}
|
|
20
20
|
from '../colors' ;
|
|
21
21
|
|
|
22
|
+
import { CENTER , END , START } from '../enums/alignments' ;
|
|
23
|
+
|
|
22
24
|
// Colors
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -79,6 +81,27 @@ const positionMap =
|
|
|
79
81
|
[ TOP ] : 'tooltip-top' ,
|
|
80
82
|
} ;
|
|
81
83
|
|
|
84
|
+
// Alignments
|
|
85
|
+
|
|
86
|
+
export { CENTER , END , START } from '../enums/alignments' ;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @typedef {'start' | 'center' | 'end'} TooltipAlignment
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Valid tooltip alignments (independent from the position axis).
|
|
94
|
+
* @type {TooltipAlignment[]}
|
|
95
|
+
*/
|
|
96
|
+
export const alignments = [ START , CENTER , END ] ;
|
|
97
|
+
|
|
98
|
+
const alignmentMap =
|
|
99
|
+
{
|
|
100
|
+
[ START ] : 'tooltip-start' ,
|
|
101
|
+
[ CENTER ] : 'tooltip-center' ,
|
|
102
|
+
[ END ] : 'tooltip-end' ,
|
|
103
|
+
} ;
|
|
104
|
+
|
|
82
105
|
export const TOOLTIP = 'tooltip' ;
|
|
83
106
|
export const TOOLTIP_CONTENT = 'tooltip-content' ;
|
|
84
107
|
|
|
@@ -86,6 +109,7 @@ export const TOOLTIP_CONTENT = 'tooltip-content' ;
|
|
|
86
109
|
* Generates a DaisyUI tooltip className expression.
|
|
87
110
|
*
|
|
88
111
|
* @param {Object} [props]
|
|
112
|
+
* @param {TooltipAlignment} [props.align] - Tooltip alignment ('start' | 'center' | 'end').
|
|
89
113
|
* @param {Object} [props.after] - Class definitions to append.
|
|
90
114
|
* @param {Object} [props.before] - Class definitions to prepend.
|
|
91
115
|
* @param {string} [props.beforeClassName] - ClassName to prepend.
|
|
@@ -104,12 +128,13 @@ export const TOOLTIP_CONTENT = 'tooltip-content' ;
|
|
|
104
128
|
* getTooltipClassNames({ position: 'bottom' , color: 'error' }) ;
|
|
105
129
|
* // → 'tooltip tooltip-error tooltip-bottom'
|
|
106
130
|
*
|
|
107
|
-
* getTooltipClassNames({
|
|
108
|
-
* // → 'tooltip tooltip-info tooltip-
|
|
131
|
+
* getTooltipClassNames({ position: 'top' , align: 'start' , color: 'info' }) ;
|
|
132
|
+
* // → 'tooltip tooltip-info tooltip-top tooltip-start'
|
|
109
133
|
* ```
|
|
110
134
|
*/
|
|
111
135
|
const getTooltipClassNames =
|
|
112
136
|
({
|
|
137
|
+
align ,
|
|
113
138
|
after ,
|
|
114
139
|
before ,
|
|
115
140
|
beforeClassName ,
|
|
@@ -127,6 +152,7 @@ const getTooltipClassNames =
|
|
|
127
152
|
|
|
128
153
|
...!!colorMap[color] && { [colorMap[color]] : true } ,
|
|
129
154
|
...!!positionMap[position] && { [positionMap[position]] : true } ,
|
|
155
|
+
...!!alignmentMap[align] && { [alignmentMap[align]] : true } ,
|
|
130
156
|
...open === true && { 'tooltip-open' : true } ,
|
|
131
157
|
|
|
132
158
|
...after ,
|
package/src/version.js
CHANGED