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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oihana-next-ui",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "private": false,
5
5
  "description": "Oihana Next.js UI component library — reusable components, hooks and utilities built with React 19, Next.js, Tailwind CSS and DaisyUI",
6
6
  "author": {
@@ -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
@@ -75,7 +75,7 @@ const MenuLink =
75
75
  <Tooltip
76
76
  className={ tooltipClassName }
77
77
  color={ tooltipColor }
78
- label={ tooltip }
78
+ tip={ tooltip }
79
79
  position={ tooltipPosition }
80
80
  show={ showTooltip && !disabled }
81
81
  >
@@ -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="flex-1 max-w-full">
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', 'sm', 'md', 'lg', 'xl'
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 from 'vegas-js-core/src/strings/notEmpty' ;
10
- import startsWith from 'vegas-js-core/src/strings/startsWith' ;
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 ) && startsWith( pathname , 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
- } from './helpers/collapseStorage' ;
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
- } from './helpers/constants' ;
20
+ }
21
+ from './helpers/constants' ;
22
+
18
23
  import findActiveAncestorIds from './helpers/findActiveAncestorIds' ;
19
- import mapI18nItem from './helpers/mapI18nItem' ;
20
- import resolveCollapseOpen from './helpers/resolveCollapseOpen' ;
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
- navigation ,
192
- setNavigation ,
193
- defaultMode ,
206
+ activePath ,
194
207
  collapses ,
195
- setCollapse ,
208
+ defaultMode ,
196
209
  getCollapseOpen ,
210
+ navigation ,
197
211
  pathname ,
212
+ setNavigation ,
213
+ setCollapse ,
198
214
  })
199
- , [ navigation , defaultMode , collapses , setCollapse , getCollapseOpen , pathname ] ) ;
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
- const active = startsWith( pathname , path ) ;
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
- ...!!colorMap[color] && { [ colorMap[color] ] : true },
115
- ...!!sizeMap[size] && { [ sizeMap[size] ] : true },
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
- const sizeMap =
24
- {
25
- [ XS ] : 'rating-xs',
26
- [ SM ] : 'rating-sm',
27
- [ MD ] : 'rating-md',
28
- [ LG ] : 'rating-lg',
29
- [ XL ] : 'rating-xl',
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 && { [ RATING_HALF ] : true } ,
77
- ...!!sizeMap[size] && { [ sizeMap[size] ] : true } ,
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({ open: true , color: 'info' , position: 'left' }) ;
108
- * // → 'tooltip tooltip-info tooltip-left tooltip-open'
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
@@ -1,3 +1,3 @@
1
- const version = "0.2.4" ;
1
+ const version = "0.2.5" ;
2
2
 
3
3
  export default version ;