@gnome-ui/react 1.40.1 → 1.41.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 +42 -0
- package/dist/components/AvatarRotator/AvatarRotator.cjs +2 -0
- package/dist/components/AvatarRotator/AvatarRotator.cjs.map +1 -0
- package/dist/components/AvatarRotator/AvatarRotator.d.ts +36 -0
- package/dist/components/AvatarRotator/AvatarRotator.js +84 -0
- package/dist/components/AvatarRotator/AvatarRotator.js.map +1 -0
- package/dist/components/AvatarRotator/AvatarRotator.module.css.cjs +2 -0
- package/dist/components/AvatarRotator/AvatarRotator.module.css.cjs.map +1 -0
- package/dist/components/AvatarRotator/AvatarRotator.module.css.js +13 -0
- package/dist/components/AvatarRotator/AvatarRotator.module.css.js.map +1 -0
- package/dist/components/AvatarRotator/index.d.ts +2 -0
- package/dist/components/AvatarRotator.cjs +1 -0
- package/dist/components/AvatarRotator.d.ts +2 -0
- package/dist/components/AvatarRotator.js +2 -0
- package/dist/components/Chip/Chip.cjs +1 -1
- package/dist/components/Chip/Chip.cjs.map +1 -1
- package/dist/components/Chip/Chip.js +19 -19
- package/dist/components/ContributionGraph/ContributionGraph.cjs +1 -1
- package/dist/components/ContributionGraph/ContributionGraph.cjs.map +1 -1
- package/dist/components/ContributionGraph/ContributionGraph.js +108 -117
- package/dist/components/ContributionGraph/ContributionGraph.js.map +1 -1
- package/dist/components/CountDownTimer/CountDownTimer.cjs +1 -1
- package/dist/components/CountDownTimer/CountDownTimer.cjs.map +1 -1
- package/dist/components/CountDownTimer/CountDownTimer.js +37 -28
- package/dist/components/CountDownTimer/CountDownTimer.js.map +1 -1
- package/dist/components/Dropdown/Dropdown.cjs +1 -1
- package/dist/components/Dropdown/Dropdown.cjs.map +1 -1
- package/dist/components/Dropdown/Dropdown.js +45 -46
- package/dist/components/Dropdown/Dropdown.js.map +1 -1
- package/dist/components/GnomeProvider/GnomeContext.cjs +2 -0
- package/dist/components/GnomeProvider/GnomeContext.cjs.map +1 -0
- package/dist/components/GnomeProvider/GnomeContext.d.ts +16 -0
- package/dist/components/GnomeProvider/GnomeContext.js +40 -0
- package/dist/components/GnomeProvider/GnomeContext.js.map +1 -0
- package/dist/components/GnomeProvider/GnomeProvider.cjs +2 -0
- package/dist/components/GnomeProvider/GnomeProvider.cjs.map +1 -0
- package/dist/components/GnomeProvider/GnomeProvider.d.ts +24 -0
- package/dist/components/GnomeProvider/GnomeProvider.js +25 -0
- package/dist/components/GnomeProvider/GnomeProvider.js.map +1 -0
- package/dist/components/PasswordEntryRow/PasswordEntryRow.cjs +1 -1
- package/dist/components/PasswordEntryRow/PasswordEntryRow.cjs.map +1 -1
- package/dist/components/PasswordEntryRow/PasswordEntryRow.js +11 -12
- package/dist/components/PasswordEntryRow/PasswordEntryRow.js.map +1 -1
- package/dist/components/SearchBar/SearchBar.cjs +1 -1
- package/dist/components/SearchBar/SearchBar.cjs.map +1 -1
- package/dist/components/SearchBar/SearchBar.js +38 -39
- package/dist/components/SearchBar/SearchBar.js.map +1 -1
- package/dist/components/Sidebar/SidebarSection.cjs +1 -1
- package/dist/components/Sidebar/SidebarSection.cjs.map +1 -1
- package/dist/components/Sidebar/SidebarSection.js +41 -42
- package/dist/components/Sidebar/SidebarSection.js.map +1 -1
- package/dist/components/Toast/Toast.cjs +1 -1
- package/dist/components/Toast/Toast.cjs.map +1 -1
- package/dist/components/Toast/Toast.js +18 -18
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks.cjs +1 -1
- package/dist/hooks.js +3 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +90 -87
- package/dist/style.css +1 -1
- package/package.json +16 -1
- package/dist/icons/dist/icons/Check.cjs +0 -2
- package/dist/icons/dist/icons/Check.cjs.map +0 -1
- package/dist/icons/dist/icons/Check.js +0 -12
- package/dist/icons/dist/icons/Check.js.map +0 -1
- package/dist/icons/dist/icons/Close.cjs +0 -2
- package/dist/icons/dist/icons/Close.cjs.map +0 -1
- package/dist/icons/dist/icons/Close.js +0 -12
- package/dist/icons/dist/icons/Close.js.map +0 -1
- package/dist/icons/dist/icons/PanDown.cjs +0 -2
- package/dist/icons/dist/icons/PanDown.cjs.map +0 -1
- package/dist/icons/dist/icons/PanDown.js +0 -12
- package/dist/icons/dist/icons/PanDown.js.map +0 -1
- package/dist/icons/dist/icons/PanUp.cjs +0 -2
- package/dist/icons/dist/icons/PanUp.cjs.map +0 -1
- package/dist/icons/dist/icons/PanUp.js +0 -12
- package/dist/icons/dist/icons/PanUp.js.map +0 -1
- package/dist/icons/dist/icons/Search.cjs +0 -2
- package/dist/icons/dist/icons/Search.cjs.map +0 -1
- package/dist/icons/dist/icons/Search.js +0 -12
- package/dist/icons/dist/icons/Search.js.map +0 -1
- package/dist/icons/dist/icons/ViewConceal.cjs +0 -2
- package/dist/icons/dist/icons/ViewConceal.cjs.map +0 -1
- package/dist/icons/dist/icons/ViewConceal.js +0 -12
- package/dist/icons/dist/icons/ViewConceal.js.map +0 -1
- package/dist/icons/dist/icons/ViewReveal.cjs +0 -2
- package/dist/icons/dist/icons/ViewReveal.cjs.map +0 -1
- package/dist/icons/dist/icons/ViewReveal.js +0 -12
- package/dist/icons/dist/icons/ViewReveal.js.map +0 -1
package/README.md
CHANGED
|
@@ -38,6 +38,47 @@ export default function App() {
|
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## Provider & formatting
|
|
42
|
+
|
|
43
|
+
`GnomeProvider` supplies locale, text direction, and default `Intl` formatting options to gnome-ui components. When `locale` is omitted, the browser locale is used.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { GnomeProvider } from "@gnome-ui/react";
|
|
47
|
+
|
|
48
|
+
export default function App() {
|
|
49
|
+
return (
|
|
50
|
+
<GnomeProvider
|
|
51
|
+
locale="en-US"
|
|
52
|
+
numberFormat={{ notation: "compact", compactDisplay: "short" }}
|
|
53
|
+
dateTimeFormat={{ dateStyle: "medium" }}
|
|
54
|
+
>
|
|
55
|
+
{/* CounterCard, StatCard, charts, CountDownTimer, etc. inherit this. */}
|
|
56
|
+
</GnomeProvider>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use compact notation for values like `1K`; omit `numberFormat` or set `notation: "standard"` for full values like `1,000`.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<GnomeProvider locale="en-US" numberFormat={{ notation: "standard" }}>
|
|
65
|
+
{/* numbers render as 1,000 */}
|
|
66
|
+
</GnomeProvider>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Custom components can consume the same defaults:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { useNumberFormatter, useDateTimeFormatter } from "@gnome-ui/react";
|
|
73
|
+
|
|
74
|
+
function Metric({ value, date }: { value: number; date: Date }) {
|
|
75
|
+
const number = useNumberFormatter({ maximumFractionDigits: 1 });
|
|
76
|
+
const day = useDateTimeFormatter({ weekday: "short" });
|
|
77
|
+
|
|
78
|
+
return `${number.format(value)} · ${day.format(date)}`;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
41
82
|
## Tree-shaking
|
|
42
83
|
|
|
43
84
|
The package ships per-component entry points, so bundlers (webpack, Rollup, esbuild, Vite) can eliminate unused components automatically. Named imports from the root entry work with any bundler that respects `"sideEffects"`:
|
|
@@ -83,6 +124,7 @@ import { Button } from "@gnome-ui/react/components/Button";
|
|
|
83
124
|
| [`Text`](https://eljijuna.github.io/gnome-ui/?path=/docs/components-text--docs) | 12 Adwaita type styles: large-title, title-1…4, heading, body, caption… |
|
|
84
125
|
| [`Icon`](https://eljijuna.github.io/gnome-ui/?path=/docs/components-icon--docs) | Inline SVG icon. Accepts `@gnome-ui/icons` definitions, [`simple-icons`](https://simpleicons.org/) icons, or any `{ path }` object; inherits `currentColor` |
|
|
85
126
|
| [`Avatar`](https://eljijuna.github.io/gnome-ui/?path=/docs/components-avatar--docs) | Circular user image with deterministic-color initials fallback |
|
|
127
|
+
| [`AvatarRotator`](https://eljijuna.github.io/gnome-ui/?path=/docs/components-avatarrotator--docs) | Single avatar surface that crossfades through multiple image sources |
|
|
86
128
|
| [`Badge`](https://eljijuna.github.io/gnome-ui/?path=/docs/components-badge--docs) | Counter or status dot, optionally anchored over another element |
|
|
87
129
|
| [`Blockquote`](https://eljijuna.github.io/gnome-ui/?path=/docs/components-blockquote--docs) | Styled pull-quote with left-border accent; default, info, warning, error, success variants; optional icon and attribution |
|
|
88
130
|
| [`Spinner`](https://eljijuna.github.io/gnome-ui/?path=/docs/components-spinner--docs) | Indeterminate loading indicator; sm/md/lg sizes |
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require(`../Avatar/Avatar.cjs`),t=require(`./AvatarRotator.module.css.cjs`);let n=require(`react`),r=require(`react/jsx-runtime`);function i(e,t){return t<=0?0:(e%t+t)%t}function a(){let[e,t]=(0,n.useState)(!1);return(0,n.useEffect)(()=>{if(typeof window>`u`||!window.matchMedia)return;let e=window.matchMedia(`(prefers-reduced-motion: reduce)`);t(e.matches);let n=()=>t(e.matches);return e.addEventListener?.(`change`,n),()=>e.removeEventListener?.(`change`,n)},[]),e}function o({name:o=``,avatars:s=[],alt:c,size:l=`md`,color:u,interval:d=3e3,transitionDuration:f=240,pauseOnHover:p=!0,activeIndex:m,defaultActiveIndex:h=0,onIndexChange:g,className:_,style:v,onMouseEnter:y,onMouseLeave:b,onFocus:x,onBlur:S,...C}){let w=(0,n.useMemo)(()=>s.map(e=>e.trim()).filter(Boolean),[s]),T=m!==void 0,[E,D]=(0,n.useState)(h),[O,k]=(0,n.useState)(!1),A=a(),j=i(T?m:E,w.length);if((0,n.useEffect)(()=>{if(w.length<=1||d<=0||O||A)return;let e=window.setInterval(()=>{let e=i(j+1,w.length);T||D(e),g?.(e)},d);return()=>window.clearInterval(e)},[j,d,T,O,g,A,w.length]),w.length===0)return(0,r.jsx)(e.Avatar,{name:o,alt:c,size:l,color:u,className:_,style:v,...C});let M=c??o??`Avatar`,N={...v,"--avatar-rotator-duration":`${Math.max(0,f)}ms`};return(0,r.jsx)(`span`,{role:`img`,"aria-label":M,className:[t.default.rotator,t.default[l],_].filter(Boolean).join(` `),style:N,onMouseEnter:e=>{p&&k(!0),y?.(e)},onMouseLeave:e=>{p&&k(!1),b?.(e)},onFocus:e=>{p&&k(!0),x?.(e)},onBlur:e=>{p&&k(!1),S?.(e)},...C,children:w.map((n,i)=>(0,r.jsx)(e.Avatar,{name:o,src:n,alt:``,size:l,"aria-hidden":`true`,className:[t.default.layer,i===j?t.default.active:null].filter(Boolean).join(` `)},`${n}-${i}`))})}exports.AvatarRotator=o;
|
|
2
|
+
//# sourceMappingURL=AvatarRotator.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvatarRotator.cjs","names":[],"sources":["../../../src/components/AvatarRotator/AvatarRotator.tsx"],"sourcesContent":["import {\n useEffect,\n useMemo,\n useState,\n type CSSProperties,\n type HTMLAttributes,\n} from \"react\";\nimport { Avatar, type AvatarColor, type AvatarSize } from \"../Avatar\";\nimport styles from \"./AvatarRotator.module.css\";\n\nexport interface AvatarRotatorProps extends HTMLAttributes<HTMLSpanElement> {\n /** Full name used for the accessible label and initials fallback. */\n name?: string;\n /** Image URLs to rotate through. */\n avatars?: string[];\n /** Accessible label. Defaults to `name`. */\n alt?: string;\n /** Size of the avatar. Defaults to `\"md\"`. */\n size?: AvatarSize;\n /** Fallback initials color when no avatar image is available. */\n color?: AvatarColor;\n /** Time between avatar changes in milliseconds. Defaults to `3000`. */\n interval?: number;\n /** Crossfade duration in milliseconds. Defaults to `240`. */\n transitionDuration?: number;\n /** Pause automatic rotation while hovered or focused. Defaults to `true`. */\n pauseOnHover?: boolean;\n /**\n * Controlled active avatar index.\n * When omitted the rotator manages index state internally.\n */\n activeIndex?: number;\n /** Initial active avatar index for uncontrolled usage. Defaults to `0`. */\n defaultActiveIndex?: number;\n /** Called when the active avatar changes. */\n onIndexChange?: (index: number) => void;\n}\n\nfunction clampIndex(index: number, length: number) {\n if (length <= 0) return 0;\n return ((index % length) + length) % length;\n}\n\nfunction usePrefersReducedMotion() {\n const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);\n\n useEffect(() => {\n if (typeof window === \"undefined\" || !window.matchMedia) return;\n\n const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n setPrefersReducedMotion(query.matches);\n\n const handleChange = () => setPrefersReducedMotion(query.matches);\n query.addEventListener?.(\"change\", handleChange);\n return () => query.removeEventListener?.(\"change\", handleChange);\n }, []);\n\n return prefersReducedMotion;\n}\n\n/**\n * Single avatar surface that rotates through multiple image sources.\n *\n * Keeps `Avatar` focused on rendering one identity, while this component owns\n * timing, crossfade animation, and pause behavior.\n */\nexport function AvatarRotator({\n name = \"\",\n avatars = [],\n alt,\n size = \"md\",\n color,\n interval = 3000,\n transitionDuration = 240,\n pauseOnHover = true,\n activeIndex,\n defaultActiveIndex = 0,\n onIndexChange,\n className,\n style,\n onMouseEnter,\n onMouseLeave,\n onFocus,\n onBlur,\n ...props\n}: AvatarRotatorProps) {\n const sources = useMemo(\n () => avatars.map((src) => src.trim()).filter(Boolean),\n [avatars],\n );\n const isControlled = activeIndex !== undefined;\n const [internalIndex, setInternalIndex] = useState(defaultActiveIndex);\n const [isPaused, setIsPaused] = useState(false);\n const prefersReducedMotion = usePrefersReducedMotion();\n const currentIndex = clampIndex(\n isControlled ? activeIndex : internalIndex,\n sources.length,\n );\n\n useEffect(() => {\n if (sources.length <= 1) return;\n if (interval <= 0) return;\n if (isPaused || prefersReducedMotion) return;\n\n const timer = window.setInterval(() => {\n const nextIndex = clampIndex(currentIndex + 1, sources.length);\n if (!isControlled) setInternalIndex(nextIndex);\n onIndexChange?.(nextIndex);\n }, interval);\n\n return () => window.clearInterval(timer);\n }, [\n currentIndex,\n interval,\n isControlled,\n isPaused,\n onIndexChange,\n prefersReducedMotion,\n sources.length,\n ]);\n\n if (sources.length === 0) {\n return (\n <Avatar\n name={name}\n alt={alt}\n size={size}\n color={color}\n className={className}\n style={style}\n {...props}\n />\n );\n }\n\n const label = alt ?? name ?? \"Avatar\";\n const transitionStyle = {\n ...style,\n \"--avatar-rotator-duration\": `${Math.max(0, transitionDuration)}ms`,\n } as CSSProperties;\n\n return (\n <span\n role=\"img\"\n aria-label={label}\n className={[styles.rotator, styles[size], className]\n .filter(Boolean)\n .join(\" \")}\n style={transitionStyle}\n onMouseEnter={(event) => {\n if (pauseOnHover) setIsPaused(true);\n onMouseEnter?.(event);\n }}\n onMouseLeave={(event) => {\n if (pauseOnHover) setIsPaused(false);\n onMouseLeave?.(event);\n }}\n onFocus={(event) => {\n if (pauseOnHover) setIsPaused(true);\n onFocus?.(event);\n }}\n onBlur={(event) => {\n if (pauseOnHover) setIsPaused(false);\n onBlur?.(event);\n }}\n {...props}\n >\n {sources.map((src, index) => (\n <Avatar\n key={`${src}-${index}`}\n name={name}\n src={src}\n alt=\"\"\n size={size}\n aria-hidden=\"true\"\n className={[\n styles.layer,\n index === currentIndex ? styles.active : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n />\n ))}\n </span>\n );\n}\n"],"mappings":"0IAsCA,SAAS,EAAW,EAAe,EAAgB,CAEjD,OADI,GAAU,EAAU,GACf,EAAQ,EAAU,GAAU,EAGvC,SAAS,GAA0B,CACjC,GAAM,CAAC,EAAsB,IAAA,EAAA,EAAA,UAAoC,GAAM,CAavE,OAXA,EAAA,EAAA,eAAgB,CACd,GAAI,OAAO,OAAW,KAAe,CAAC,OAAO,WAAY,OAEzD,IAAM,EAAQ,OAAO,WAAW,mCAAmC,CACnE,EAAwB,EAAM,QAAQ,CAEtC,IAAM,MAAqB,EAAwB,EAAM,QAAQ,CAEjE,OADA,EAAM,mBAAmB,SAAU,EAAa,KACnC,EAAM,sBAAsB,SAAU,EAAa,EAC/D,EAAE,CAAC,CAEC,EAST,SAAgB,EAAc,CAC5B,OAAO,GACP,UAAU,EAAE,CACZ,MACA,OAAO,KACP,QACA,WAAW,IACX,qBAAqB,IACrB,eAAe,GACf,cACA,qBAAqB,EACrB,gBACA,YACA,QACA,eACA,eACA,UACA,SACA,GAAG,GACkB,CACrB,IAAM,GAAA,EAAA,EAAA,aACE,EAAQ,IAAK,GAAQ,EAAI,MAAM,CAAC,CAAC,OAAO,QAAQ,CACtD,CAAC,EAAQ,CACV,CACK,EAAe,IAAgB,IAAA,GAC/B,CAAC,EAAe,IAAA,EAAA,EAAA,UAA6B,EAAmB,CAChE,CAAC,EAAU,IAAA,EAAA,EAAA,UAAwB,GAAM,CACzC,EAAuB,GAAyB,CAChD,EAAe,EACnB,EAAe,EAAc,EAC7B,EAAQ,OACT,CAwBD,IAtBA,EAAA,EAAA,eAAgB,CAGd,GAFI,EAAQ,QAAU,GAClB,GAAY,GACZ,GAAY,EAAsB,OAEtC,IAAM,EAAQ,OAAO,gBAAkB,CACrC,IAAM,EAAY,EAAW,EAAe,EAAG,EAAQ,OAAO,CACzD,GAAc,EAAiB,EAAU,CAC9C,IAAgB,EAAU,EACzB,EAAS,CAEZ,UAAa,OAAO,cAAc,EAAM,EACvC,CACD,EACA,EACA,EACA,EACA,EACA,EACA,EAAQ,OACT,CAAC,CAEE,EAAQ,SAAW,EACrB,OACE,EAAA,EAAA,KAAC,EAAA,OAAD,CACQ,OACD,MACC,OACC,QACI,YACJ,QACP,GAAI,EACJ,CAAA,CAIN,IAAM,EAAQ,GAAO,GAAQ,SACvB,EAAkB,CACtB,GAAG,EACH,4BAA6B,GAAG,KAAK,IAAI,EAAG,EAAmB,CAAC,IACjE,CAED,OACE,EAAA,EAAA,KAAC,OAAD,CACE,KAAK,MACL,aAAY,EACZ,UAAW,CAAC,EAAA,QAAO,QAAS,EAAA,QAAO,GAAO,EAAU,CACjD,OAAO,QAAQ,CACf,KAAK,IAAI,CACZ,MAAO,EACP,aAAe,GAAU,CACnB,GAAc,EAAY,GAAK,CACnC,IAAe,EAAM,EAEvB,aAAe,GAAU,CACnB,GAAc,EAAY,GAAM,CACpC,IAAe,EAAM,EAEvB,QAAU,GAAU,CACd,GAAc,EAAY,GAAK,CACnC,IAAU,EAAM,EAElB,OAAS,GAAU,CACb,GAAc,EAAY,GAAM,CACpC,IAAS,EAAM,EAEjB,GAAI,WAEH,EAAQ,KAAK,EAAK,KACjB,EAAA,EAAA,KAAC,EAAA,OAAD,CAEQ,OACD,MACL,IAAI,GACE,OACN,cAAY,OACZ,UAAW,CACT,EAAA,QAAO,MACP,IAAU,EAAe,EAAA,QAAO,OAAS,KAC1C,CACE,OAAO,QAAQ,CACf,KAAK,IAAI,CACZ,CAZK,GAAG,EAAI,GAAG,IAYf,CACF,CACG,CAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { HTMLAttributes } from 'react';
|
|
2
|
+
import { AvatarColor, AvatarSize } from '../Avatar';
|
|
3
|
+
export interface AvatarRotatorProps extends HTMLAttributes<HTMLSpanElement> {
|
|
4
|
+
/** Full name used for the accessible label and initials fallback. */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Image URLs to rotate through. */
|
|
7
|
+
avatars?: string[];
|
|
8
|
+
/** Accessible label. Defaults to `name`. */
|
|
9
|
+
alt?: string;
|
|
10
|
+
/** Size of the avatar. Defaults to `"md"`. */
|
|
11
|
+
size?: AvatarSize;
|
|
12
|
+
/** Fallback initials color when no avatar image is available. */
|
|
13
|
+
color?: AvatarColor;
|
|
14
|
+
/** Time between avatar changes in milliseconds. Defaults to `3000`. */
|
|
15
|
+
interval?: number;
|
|
16
|
+
/** Crossfade duration in milliseconds. Defaults to `240`. */
|
|
17
|
+
transitionDuration?: number;
|
|
18
|
+
/** Pause automatic rotation while hovered or focused. Defaults to `true`. */
|
|
19
|
+
pauseOnHover?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Controlled active avatar index.
|
|
22
|
+
* When omitted the rotator manages index state internally.
|
|
23
|
+
*/
|
|
24
|
+
activeIndex?: number;
|
|
25
|
+
/** Initial active avatar index for uncontrolled usage. Defaults to `0`. */
|
|
26
|
+
defaultActiveIndex?: number;
|
|
27
|
+
/** Called when the active avatar changes. */
|
|
28
|
+
onIndexChange?: (index: number) => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Single avatar surface that rotates through multiple image sources.
|
|
32
|
+
*
|
|
33
|
+
* Keeps `Avatar` focused on rendering one identity, while this component owns
|
|
34
|
+
* timing, crossfade animation, and pause behavior.
|
|
35
|
+
*/
|
|
36
|
+
export declare function AvatarRotator({ name, avatars, alt, size, color, interval, transitionDuration, pauseOnHover, activeIndex, defaultActiveIndex, onIndexChange, className, style, onMouseEnter, onMouseLeave, onFocus, onBlur, ...props }: AvatarRotatorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Avatar as e } from "../Avatar/Avatar.js";
|
|
2
|
+
import t from "./AvatarRotator.module.css.js";
|
|
3
|
+
import { useEffect as n, useMemo as r, useState as i } from "react";
|
|
4
|
+
import { jsx as a } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/AvatarRotator/AvatarRotator.tsx
|
|
6
|
+
function o(e, t) {
|
|
7
|
+
return t <= 0 ? 0 : (e % t + t) % t;
|
|
8
|
+
}
|
|
9
|
+
function s() {
|
|
10
|
+
let [e, t] = i(!1);
|
|
11
|
+
return n(() => {
|
|
12
|
+
if (typeof window > "u" || !window.matchMedia) return;
|
|
13
|
+
let e = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
14
|
+
t(e.matches);
|
|
15
|
+
let n = () => t(e.matches);
|
|
16
|
+
return e.addEventListener?.("change", n), () => e.removeEventListener?.("change", n);
|
|
17
|
+
}, []), e;
|
|
18
|
+
}
|
|
19
|
+
function c({ name: c = "", avatars: l = [], alt: u, size: d = "md", color: f, interval: p = 3e3, transitionDuration: m = 240, pauseOnHover: h = !0, activeIndex: g, defaultActiveIndex: _ = 0, onIndexChange: v, className: y, style: b, onMouseEnter: x, onMouseLeave: S, onFocus: C, onBlur: w, ...T }) {
|
|
20
|
+
let E = r(() => l.map((e) => e.trim()).filter(Boolean), [l]), D = g !== void 0, [O, k] = i(_), [A, j] = i(!1), M = s(), N = o(D ? g : O, E.length);
|
|
21
|
+
if (n(() => {
|
|
22
|
+
if (E.length <= 1 || p <= 0 || A || M) return;
|
|
23
|
+
let e = window.setInterval(() => {
|
|
24
|
+
let e = o(N + 1, E.length);
|
|
25
|
+
D || k(e), v?.(e);
|
|
26
|
+
}, p);
|
|
27
|
+
return () => window.clearInterval(e);
|
|
28
|
+
}, [
|
|
29
|
+
N,
|
|
30
|
+
p,
|
|
31
|
+
D,
|
|
32
|
+
A,
|
|
33
|
+
v,
|
|
34
|
+
M,
|
|
35
|
+
E.length
|
|
36
|
+
]), E.length === 0) return /* @__PURE__ */ a(e, {
|
|
37
|
+
name: c,
|
|
38
|
+
alt: u,
|
|
39
|
+
size: d,
|
|
40
|
+
color: f,
|
|
41
|
+
className: y,
|
|
42
|
+
style: b,
|
|
43
|
+
...T
|
|
44
|
+
});
|
|
45
|
+
let P = u ?? c ?? "Avatar", F = {
|
|
46
|
+
...b,
|
|
47
|
+
"--avatar-rotator-duration": `${Math.max(0, m)}ms`
|
|
48
|
+
};
|
|
49
|
+
return /* @__PURE__ */ a("span", {
|
|
50
|
+
role: "img",
|
|
51
|
+
"aria-label": P,
|
|
52
|
+
className: [
|
|
53
|
+
t.rotator,
|
|
54
|
+
t[d],
|
|
55
|
+
y
|
|
56
|
+
].filter(Boolean).join(" "),
|
|
57
|
+
style: F,
|
|
58
|
+
onMouseEnter: (e) => {
|
|
59
|
+
h && j(!0), x?.(e);
|
|
60
|
+
},
|
|
61
|
+
onMouseLeave: (e) => {
|
|
62
|
+
h && j(!1), S?.(e);
|
|
63
|
+
},
|
|
64
|
+
onFocus: (e) => {
|
|
65
|
+
h && j(!0), C?.(e);
|
|
66
|
+
},
|
|
67
|
+
onBlur: (e) => {
|
|
68
|
+
h && j(!1), w?.(e);
|
|
69
|
+
},
|
|
70
|
+
...T,
|
|
71
|
+
children: E.map((n, r) => /* @__PURE__ */ a(e, {
|
|
72
|
+
name: c,
|
|
73
|
+
src: n,
|
|
74
|
+
alt: "",
|
|
75
|
+
size: d,
|
|
76
|
+
"aria-hidden": "true",
|
|
77
|
+
className: [t.layer, r === N ? t.active : null].filter(Boolean).join(" ")
|
|
78
|
+
}, `${n}-${r}`))
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
export { c as AvatarRotator };
|
|
83
|
+
|
|
84
|
+
//# sourceMappingURL=AvatarRotator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvatarRotator.js","names":[],"sources":["../../../src/components/AvatarRotator/AvatarRotator.tsx"],"sourcesContent":["import {\n useEffect,\n useMemo,\n useState,\n type CSSProperties,\n type HTMLAttributes,\n} from \"react\";\nimport { Avatar, type AvatarColor, type AvatarSize } from \"../Avatar\";\nimport styles from \"./AvatarRotator.module.css\";\n\nexport interface AvatarRotatorProps extends HTMLAttributes<HTMLSpanElement> {\n /** Full name used for the accessible label and initials fallback. */\n name?: string;\n /** Image URLs to rotate through. */\n avatars?: string[];\n /** Accessible label. Defaults to `name`. */\n alt?: string;\n /** Size of the avatar. Defaults to `\"md\"`. */\n size?: AvatarSize;\n /** Fallback initials color when no avatar image is available. */\n color?: AvatarColor;\n /** Time between avatar changes in milliseconds. Defaults to `3000`. */\n interval?: number;\n /** Crossfade duration in milliseconds. Defaults to `240`. */\n transitionDuration?: number;\n /** Pause automatic rotation while hovered or focused. Defaults to `true`. */\n pauseOnHover?: boolean;\n /**\n * Controlled active avatar index.\n * When omitted the rotator manages index state internally.\n */\n activeIndex?: number;\n /** Initial active avatar index for uncontrolled usage. Defaults to `0`. */\n defaultActiveIndex?: number;\n /** Called when the active avatar changes. */\n onIndexChange?: (index: number) => void;\n}\n\nfunction clampIndex(index: number, length: number) {\n if (length <= 0) return 0;\n return ((index % length) + length) % length;\n}\n\nfunction usePrefersReducedMotion() {\n const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);\n\n useEffect(() => {\n if (typeof window === \"undefined\" || !window.matchMedia) return;\n\n const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n setPrefersReducedMotion(query.matches);\n\n const handleChange = () => setPrefersReducedMotion(query.matches);\n query.addEventListener?.(\"change\", handleChange);\n return () => query.removeEventListener?.(\"change\", handleChange);\n }, []);\n\n return prefersReducedMotion;\n}\n\n/**\n * Single avatar surface that rotates through multiple image sources.\n *\n * Keeps `Avatar` focused on rendering one identity, while this component owns\n * timing, crossfade animation, and pause behavior.\n */\nexport function AvatarRotator({\n name = \"\",\n avatars = [],\n alt,\n size = \"md\",\n color,\n interval = 3000,\n transitionDuration = 240,\n pauseOnHover = true,\n activeIndex,\n defaultActiveIndex = 0,\n onIndexChange,\n className,\n style,\n onMouseEnter,\n onMouseLeave,\n onFocus,\n onBlur,\n ...props\n}: AvatarRotatorProps) {\n const sources = useMemo(\n () => avatars.map((src) => src.trim()).filter(Boolean),\n [avatars],\n );\n const isControlled = activeIndex !== undefined;\n const [internalIndex, setInternalIndex] = useState(defaultActiveIndex);\n const [isPaused, setIsPaused] = useState(false);\n const prefersReducedMotion = usePrefersReducedMotion();\n const currentIndex = clampIndex(\n isControlled ? activeIndex : internalIndex,\n sources.length,\n );\n\n useEffect(() => {\n if (sources.length <= 1) return;\n if (interval <= 0) return;\n if (isPaused || prefersReducedMotion) return;\n\n const timer = window.setInterval(() => {\n const nextIndex = clampIndex(currentIndex + 1, sources.length);\n if (!isControlled) setInternalIndex(nextIndex);\n onIndexChange?.(nextIndex);\n }, interval);\n\n return () => window.clearInterval(timer);\n }, [\n currentIndex,\n interval,\n isControlled,\n isPaused,\n onIndexChange,\n prefersReducedMotion,\n sources.length,\n ]);\n\n if (sources.length === 0) {\n return (\n <Avatar\n name={name}\n alt={alt}\n size={size}\n color={color}\n className={className}\n style={style}\n {...props}\n />\n );\n }\n\n const label = alt ?? name ?? \"Avatar\";\n const transitionStyle = {\n ...style,\n \"--avatar-rotator-duration\": `${Math.max(0, transitionDuration)}ms`,\n } as CSSProperties;\n\n return (\n <span\n role=\"img\"\n aria-label={label}\n className={[styles.rotator, styles[size], className]\n .filter(Boolean)\n .join(\" \")}\n style={transitionStyle}\n onMouseEnter={(event) => {\n if (pauseOnHover) setIsPaused(true);\n onMouseEnter?.(event);\n }}\n onMouseLeave={(event) => {\n if (pauseOnHover) setIsPaused(false);\n onMouseLeave?.(event);\n }}\n onFocus={(event) => {\n if (pauseOnHover) setIsPaused(true);\n onFocus?.(event);\n }}\n onBlur={(event) => {\n if (pauseOnHover) setIsPaused(false);\n onBlur?.(event);\n }}\n {...props}\n >\n {sources.map((src, index) => (\n <Avatar\n key={`${src}-${index}`}\n name={name}\n src={src}\n alt=\"\"\n size={size}\n aria-hidden=\"true\"\n className={[\n styles.layer,\n index === currentIndex ? styles.active : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n />\n ))}\n </span>\n );\n}\n"],"mappings":";;;;;AAsCA,SAAS,EAAW,GAAe,GAAgB;AAEjD,QADI,KAAU,IAAU,KACf,IAAQ,IAAU,KAAU;;AAGvC,SAAS,IAA0B;CACjC,IAAM,CAAC,GAAsB,KAA2B,EAAS,GAAM;AAavE,QAXA,QAAgB;AACd,MAAI,OAAO,SAAW,OAAe,CAAC,OAAO,WAAY;EAEzD,IAAM,IAAQ,OAAO,WAAW,mCAAmC;AACnE,IAAwB,EAAM,QAAQ;EAEtC,IAAM,UAAqB,EAAwB,EAAM,QAAQ;AAEjE,SADA,EAAM,mBAAmB,UAAU,EAAa,QACnC,EAAM,sBAAsB,UAAU,EAAa;IAC/D,EAAE,CAAC,EAEC;;AAST,SAAgB,EAAc,EAC5B,UAAO,IACP,aAAU,EAAE,EACZ,QACA,UAAO,MACP,UACA,cAAW,KACX,wBAAqB,KACrB,kBAAe,IACf,gBACA,wBAAqB,GACrB,kBACA,cACA,UACA,iBACA,iBACA,YACA,WACA,GAAG,KACkB;CACrB,IAAM,IAAU,QACR,EAAQ,KAAK,MAAQ,EAAI,MAAM,CAAC,CAAC,OAAO,QAAQ,EACtD,CAAC,EAAQ,CACV,EACK,IAAe,MAAgB,KAAA,GAC/B,CAAC,GAAe,KAAoB,EAAS,EAAmB,EAChE,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,IAAuB,GAAyB,EAChD,IAAe,EACnB,IAAe,IAAc,GAC7B,EAAQ,OACT;AAwBD,KAtBA,QAAgB;AAGd,MAFI,EAAQ,UAAU,KAClB,KAAY,KACZ,KAAY,EAAsB;EAEtC,IAAM,IAAQ,OAAO,kBAAkB;GACrC,IAAM,IAAY,EAAW,IAAe,GAAG,EAAQ,OAAO;AAE9D,GADK,KAAc,EAAiB,EAAU,EAC9C,IAAgB,EAAU;KACzB,EAAS;AAEZ,eAAa,OAAO,cAAc,EAAM;IACvC;EACD;EACA;EACA;EACA;EACA;EACA;EACA,EAAQ;EACT,CAAC,EAEE,EAAQ,WAAW,EACrB,QACE,kBAAC,GAAD;EACQ;EACD;EACC;EACC;EACI;EACJ;EACP,GAAI;EACJ,CAAA;CAIN,IAAM,IAAQ,KAAO,KAAQ,UACvB,IAAkB;EACtB,GAAG;EACH,6BAA6B,GAAG,KAAK,IAAI,GAAG,EAAmB,CAAC;EACjE;AAED,QACE,kBAAC,QAAD;EACE,MAAK;EACL,cAAY;EACZ,WAAW;GAAC,EAAO;GAAS,EAAO;GAAO;GAAU,CACjD,OAAO,QAAQ,CACf,KAAK,IAAI;EACZ,OAAO;EACP,eAAe,MAAU;AAEvB,GADI,KAAc,EAAY,GAAK,EACnC,IAAe,EAAM;;EAEvB,eAAe,MAAU;AAEvB,GADI,KAAc,EAAY,GAAM,EACpC,IAAe,EAAM;;EAEvB,UAAU,MAAU;AAElB,GADI,KAAc,EAAY,GAAK,EACnC,IAAU,EAAM;;EAElB,SAAS,MAAU;AAEjB,GADI,KAAc,EAAY,GAAM,EACpC,IAAS,EAAM;;EAEjB,GAAI;YAEH,EAAQ,KAAK,GAAK,MACjB,kBAAC,GAAD;GAEQ;GACD;GACL,KAAI;GACE;GACN,eAAY;GACZ,WAAW,CACT,EAAO,OACP,MAAU,IAAe,EAAO,SAAS,KAC1C,CACE,OAAO,QAAQ,CACf,KAAK,IAAI;GACZ,EAZK,GAAG,EAAI,GAAG,IAYf,CACF;EACG,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvatarRotator.module.css.cjs","names":[],"sources":["../../../src/components/AvatarRotator/AvatarRotator.module.css"],"sourcesContent":["/* ─── Base ─────────────────────────────────────────────────────────────────── */\n\n.rotator {\n position: relative;\n display: inline-grid;\n border-radius: var(--gnome-radius-pill);\n overflow: hidden;\n flex-shrink: 0;\n user-select: none;\n}\n\n/* ─── Sizes ────────────────────────────────────────────────────────────────── */\n\n.sm {\n width: 24px;\n height: 24px;\n}\n\n.md {\n width: 32px;\n height: 32px;\n}\n\n.lg {\n width: 48px;\n height: 48px;\n}\n\n.xl {\n width: 64px;\n height: 64px;\n}\n\n/* ─── Layers ───────────────────────────────────────────────────────────────── */\n\n.layer {\n position: absolute;\n inset: 0;\n opacity: 0;\n transition: opacity var(--avatar-rotator-duration, 240ms) ease;\n}\n\n.active {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .layer {\n transition: none;\n }\n}\n"],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
var e = {
|
|
2
|
+
rotator: "_rotator_tvwml_3",
|
|
3
|
+
sm: "_sm_tvwml_14",
|
|
4
|
+
md: "_md_tvwml_19",
|
|
5
|
+
lg: "_lg_tvwml_24",
|
|
6
|
+
xl: "_xl_tvwml_29",
|
|
7
|
+
layer: "_layer_tvwml_36",
|
|
8
|
+
active: "_active_tvwml_43"
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { e as default };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=AvatarRotator.module.css.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvatarRotator.module.css.js","names":[],"sources":["../../../src/components/AvatarRotator/AvatarRotator.module.css"],"sourcesContent":["/* ─── Base ─────────────────────────────────────────────────────────────────── */\n\n.rotator {\n position: relative;\n display: inline-grid;\n border-radius: var(--gnome-radius-pill);\n overflow: hidden;\n flex-shrink: 0;\n user-select: none;\n}\n\n/* ─── Sizes ────────────────────────────────────────────────────────────────── */\n\n.sm {\n width: 24px;\n height: 24px;\n}\n\n.md {\n width: 32px;\n height: 32px;\n}\n\n.lg {\n width: 48px;\n height: 48px;\n}\n\n.xl {\n width: 64px;\n height: 64px;\n}\n\n/* ─── Layers ───────────────────────────────────────────────────────────────── */\n\n.layer {\n position: absolute;\n inset: 0;\n opacity: 0;\n transition: opacity var(--avatar-rotator-duration, 240ms) ease;\n}\n\n.active {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .layer {\n transition: none;\n }\n}\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./AvatarRotator/AvatarRotator.cjs`);exports.AvatarRotator=e.AvatarRotator;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require(`../Icon/Icon.cjs`),t=require(
|
|
1
|
+
const e=require(`../Icon/Icon.cjs`),t=require(`./Chip.module.css.cjs`);let n=require(`react/jsx-runtime`),r=require(`@gnome-ui/icons`);function i({label:i,icon:a,onRemove:o,selectable:s=!1,selected:c=!1,onToggle:l,disabled:u=!1,className:d,...f}){let p=s&&!o,m=[t.default.chip,c?t.default.selected:null,u?t.default.disabled:null,p?t.default.selectable:null,d].filter(Boolean).join(` `),h=(0,n.jsxs)(n.Fragment,{children:[a&&(0,n.jsx)(`span`,{className:t.default.icon,children:(0,n.jsx)(e.Icon,{icon:a,size:`sm`,"aria-hidden":!0})}),(0,n.jsx)(`span`,{className:t.default.label,children:i}),o&&(0,n.jsx)(`button`,{type:`button`,className:t.default.remove,"aria-label":`Remove ${i}`,disabled:u,onClick:e=>{e.stopPropagation(),o()},tabIndex:u?-1:0,children:(0,n.jsx)(e.Icon,{icon:r.Close,size:`sm`,"aria-hidden":!0})})]});return p?(0,n.jsx)(`button`,{type:`button`,role:`checkbox`,"aria-checked":c,"aria-label":i,disabled:u,onClick:l,className:m,...f,children:h}):(0,n.jsx)(`span`,{className:m,...f,children:h})}exports.Chip=i;
|
|
2
2
|
//# sourceMappingURL=Chip.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chip.cjs","names":[],"sources":["../../../src/components/Chip/Chip.tsx"],"sourcesContent":["import type { HTMLAttributes } from \"react\";\nimport type { IconDefinition } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport { Close } from \"@gnome-ui/icons\";\nimport styles from \"./Chip.module.css\";\n\nexport interface ChipProps extends HTMLAttributes<HTMLElement> {\n /** Text label displayed inside the chip. */\n label: string;\n /** Leading icon from `@gnome-ui/icons`. */\n icon?: IconDefinition;\n /**\n * When provided, renders a remove (×) button and calls this handler.\n * The chip root becomes a `<span>`; only the remove button is interactive.\n */\n onRemove?: () => void;\n /**\n * When true the chip renders as a toggle button.\n * Use `selected` + `onToggle` to control its state.\n */\n selectable?: boolean;\n /** Active/selected state. Only relevant when `selectable` is true. */\n selected?: boolean;\n /**\n * Called when a selectable chip is clicked.\n * Only relevant when `selectable` is true.\n */\n onToggle?: () => void;\n /** Disabled state — applies to both selectable chips and the remove button. */\n disabled?: boolean;\n}\n\n/**\n * Compact pill-shaped label for tags, filters, and selection states.\n *\n * Three usage modes:\n * - **Static** — just a visual label (no `onRemove`, no `selectable`).\n * - **Removable** — add `onRemove` to show a × button.\n * - **Selectable** — add `selectable` + `selected` + `onToggle` for toggle behaviour.\n *\n * Pair with `WrapBox` for multi-chip layouts.\n */\nexport function Chip({\n label,\n icon,\n onRemove,\n selectable = false,\n selected = false,\n onToggle,\n disabled = false,\n className,\n ...props\n}: ChipProps) {\n const isInteractive = selectable && !onRemove;\n\n const chipClasses = [\n styles.chip,\n selected ? styles.selected : null,\n disabled ? styles.disabled : null,\n isInteractive ? styles.selectable : null,\n className,\n ]\n .filter(Boolean)\n .join(\" \");\n\n const content = (\n <>\n {icon && (\n <span className={styles.icon}>\n <Icon icon={icon} size=\"sm\" aria-hidden />\n </span>\n )}\n <span className={styles.label}>{label}</span>\n {onRemove && (\n <button\n type=\"button\"\n className={styles.remove}\n aria-label={`Remove ${label}`}\n disabled={disabled}\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n tabIndex={disabled ? -1 : 0}\n >\n <Icon icon={Close} size=\"sm\" aria-hidden />\n </button>\n )}\n </>\n );\n\n if (isInteractive) {\n return (\n <button\n type=\"button\"\n role=\"checkbox\"\n aria-checked={selected}\n aria-label={label}\n disabled={disabled}\n onClick={onToggle}\n className={chipClasses}\n {...(props as HTMLAttributes<HTMLButtonElement>)}\n >\n {content}\n </button>\n );\n }\n\n return (\n <span\n className={chipClasses}\n {...props}\n >\n {content}\n </span>\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"Chip.cjs","names":[],"sources":["../../../src/components/Chip/Chip.tsx"],"sourcesContent":["import type { HTMLAttributes } from \"react\";\nimport type { IconDefinition } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport { Close } from \"@gnome-ui/icons\";\nimport styles from \"./Chip.module.css\";\n\nexport interface ChipProps extends HTMLAttributes<HTMLElement> {\n /** Text label displayed inside the chip. */\n label: string;\n /** Leading icon from `@gnome-ui/icons`. */\n icon?: IconDefinition;\n /**\n * When provided, renders a remove (×) button and calls this handler.\n * The chip root becomes a `<span>`; only the remove button is interactive.\n */\n onRemove?: () => void;\n /**\n * When true the chip renders as a toggle button.\n * Use `selected` + `onToggle` to control its state.\n */\n selectable?: boolean;\n /** Active/selected state. Only relevant when `selectable` is true. */\n selected?: boolean;\n /**\n * Called when a selectable chip is clicked.\n * Only relevant when `selectable` is true.\n */\n onToggle?: () => void;\n /** Disabled state — applies to both selectable chips and the remove button. */\n disabled?: boolean;\n}\n\n/**\n * Compact pill-shaped label for tags, filters, and selection states.\n *\n * Three usage modes:\n * - **Static** — just a visual label (no `onRemove`, no `selectable`).\n * - **Removable** — add `onRemove` to show a × button.\n * - **Selectable** — add `selectable` + `selected` + `onToggle` for toggle behaviour.\n *\n * Pair with `WrapBox` for multi-chip layouts.\n */\nexport function Chip({\n label,\n icon,\n onRemove,\n selectable = false,\n selected = false,\n onToggle,\n disabled = false,\n className,\n ...props\n}: ChipProps) {\n const isInteractive = selectable && !onRemove;\n\n const chipClasses = [\n styles.chip,\n selected ? styles.selected : null,\n disabled ? styles.disabled : null,\n isInteractive ? styles.selectable : null,\n className,\n ]\n .filter(Boolean)\n .join(\" \");\n\n const content = (\n <>\n {icon && (\n <span className={styles.icon}>\n <Icon icon={icon} size=\"sm\" aria-hidden />\n </span>\n )}\n <span className={styles.label}>{label}</span>\n {onRemove && (\n <button\n type=\"button\"\n className={styles.remove}\n aria-label={`Remove ${label}`}\n disabled={disabled}\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n tabIndex={disabled ? -1 : 0}\n >\n <Icon icon={Close} size=\"sm\" aria-hidden />\n </button>\n )}\n </>\n );\n\n if (isInteractive) {\n return (\n <button\n type=\"button\"\n role=\"checkbox\"\n aria-checked={selected}\n aria-label={label}\n disabled={disabled}\n onClick={onToggle}\n className={chipClasses}\n {...(props as HTMLAttributes<HTMLButtonElement>)}\n >\n {content}\n </button>\n );\n }\n\n return (\n <span\n className={chipClasses}\n {...props}\n >\n {content}\n </span>\n );\n}\n"],"mappings":"uIA0CA,SAAgB,EAAK,CACnB,QACA,OACA,WACA,aAAa,GACb,WAAW,GACX,WACA,WAAW,GACX,YACA,GAAG,GACS,CACZ,IAAM,EAAgB,GAAc,CAAC,EAE/B,EAAc,CAClB,EAAA,QAAO,KACP,EAAW,EAAA,QAAO,SAAW,KAC7B,EAAW,EAAA,QAAO,SAAW,KAC7B,EAAgB,EAAA,QAAO,WAAa,KACpC,EACD,CACE,OAAO,QAAQ,CACf,KAAK,IAAI,CAEN,GACJ,EAAA,EAAA,MAAA,EAAA,SAAA,CAAA,SAAA,CACG,IACC,EAAA,EAAA,KAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eACtB,EAAA,EAAA,KAAC,EAAA,KAAD,CAAY,OAAM,KAAK,KAAK,cAAA,GAAc,CAAA,CACrC,CAAA,EAET,EAAA,EAAA,KAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eAAQ,EAAa,CAAA,CAC5C,IACC,EAAA,EAAA,KAAC,SAAD,CACE,KAAK,SACL,UAAW,EAAA,QAAO,OAClB,aAAY,UAAU,IACZ,WACV,QAAU,GAAM,CACd,EAAE,iBAAiB,CACnB,GAAU,EAEZ,SAAU,EAAW,GAAK,YAE1B,EAAA,EAAA,KAAC,EAAA,KAAD,CAAM,KAAM,EAAA,MAAO,KAAK,KAAK,cAAA,GAAc,CAAA,CACpC,CAAA,CAEV,CAAA,CAAA,CAoBL,OAjBI,GAEA,EAAA,EAAA,KAAC,SAAD,CACE,KAAK,SACL,KAAK,WACL,eAAc,EACd,aAAY,EACF,WACV,QAAS,EACT,UAAW,EACX,GAAK,WAEJ,EACM,CAAA,EAKX,EAAA,EAAA,KAAC,OAAD,CACE,UAAW,EACX,GAAI,WAEH,EACI,CAAA"}
|
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
import { Icon as e } from "../Icon/Icon.js";
|
|
2
|
-
import
|
|
3
|
-
import n from "
|
|
4
|
-
import {
|
|
2
|
+
import t from "./Chip.module.css.js";
|
|
3
|
+
import { Fragment as n, jsx as r, jsxs as i } from "react/jsx-runtime";
|
|
4
|
+
import { Close as a } from "@gnome-ui/icons";
|
|
5
5
|
//#region src/components/Chip/Chip.tsx
|
|
6
6
|
function o({ label: o, icon: s, onRemove: c, selectable: l = !1, selected: u = !1, onToggle: d, disabled: f = !1, className: p, ...m }) {
|
|
7
7
|
let h = l && !c, g = [
|
|
8
|
-
|
|
9
|
-
u ?
|
|
10
|
-
f ?
|
|
11
|
-
h ?
|
|
8
|
+
t.chip,
|
|
9
|
+
u ? t.selected : null,
|
|
10
|
+
f ? t.disabled : null,
|
|
11
|
+
h ? t.selectable : null,
|
|
12
12
|
p
|
|
13
|
-
].filter(Boolean).join(" "), _ = /* @__PURE__ */
|
|
14
|
-
s && /* @__PURE__ */
|
|
15
|
-
className:
|
|
16
|
-
children: /* @__PURE__ */
|
|
13
|
+
].filter(Boolean).join(" "), _ = /* @__PURE__ */ i(n, { children: [
|
|
14
|
+
s && /* @__PURE__ */ r("span", {
|
|
15
|
+
className: t.icon,
|
|
16
|
+
children: /* @__PURE__ */ r(e, {
|
|
17
17
|
icon: s,
|
|
18
18
|
size: "sm",
|
|
19
19
|
"aria-hidden": !0
|
|
20
20
|
})
|
|
21
21
|
}),
|
|
22
|
-
/* @__PURE__ */
|
|
23
|
-
className:
|
|
22
|
+
/* @__PURE__ */ r("span", {
|
|
23
|
+
className: t.label,
|
|
24
24
|
children: o
|
|
25
25
|
}),
|
|
26
|
-
c && /* @__PURE__ */
|
|
26
|
+
c && /* @__PURE__ */ r("button", {
|
|
27
27
|
type: "button",
|
|
28
|
-
className:
|
|
28
|
+
className: t.remove,
|
|
29
29
|
"aria-label": `Remove ${o}`,
|
|
30
30
|
disabled: f,
|
|
31
31
|
onClick: (e) => {
|
|
32
32
|
e.stopPropagation(), c();
|
|
33
33
|
},
|
|
34
34
|
tabIndex: f ? -1 : 0,
|
|
35
|
-
children: /* @__PURE__ */
|
|
36
|
-
icon:
|
|
35
|
+
children: /* @__PURE__ */ r(e, {
|
|
36
|
+
icon: a,
|
|
37
37
|
size: "sm",
|
|
38
38
|
"aria-hidden": !0
|
|
39
39
|
})
|
|
40
40
|
})
|
|
41
41
|
] });
|
|
42
|
-
return h ? /* @__PURE__ */
|
|
42
|
+
return h ? /* @__PURE__ */ r("button", {
|
|
43
43
|
type: "button",
|
|
44
44
|
role: "checkbox",
|
|
45
45
|
"aria-checked": u,
|
|
@@ -49,7 +49,7 @@ function o({ label: o, icon: s, onRemove: c, selectable: l = !1, selected: u = !
|
|
|
49
49
|
className: g,
|
|
50
50
|
...m,
|
|
51
51
|
children: _
|
|
52
|
-
}) : /* @__PURE__ */
|
|
52
|
+
}) : /* @__PURE__ */ r("span", {
|
|
53
53
|
className: g,
|
|
54
54
|
...m,
|
|
55
55
|
children: _
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require(`./ContributionGraph.module.css.cjs`);let
|
|
1
|
+
const e=require(`./ContributionGraph.module.css.cjs`),t=require(`../GnomeProvider/GnomeContext.cjs`);let n=require(`react`),r=require(`react/jsx-runtime`);var i=new Date(2e3,0,2);function a(e,t){return t.format(new Date(2e3,e))}function o(e,t){let n=new Date(i);return n.setDate(i.getDate()+e),t.format(n)}var s=28,c=20,l=[`var(--gnome-card-shade-color, rgba(0,0,0,0.07))`,`var(--gnome-green-1, #8ff0a4)`,`var(--gnome-green-2, #57e389)`,`var(--gnome-green-4, #2ec27e)`,`var(--gnome-green-5, #26a269)`];function u(e){return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,`0`)}-${String(e.getDate()).padStart(2,`0`)}`}function d(e){let[t,n,r]=e.split(`-`).map(Number);return new Date(t,n-1,r)}function f(e,t){return t.format(d(e))}function p({data:i,maxLevel:d=4,colorScale:p,cellSize:m=12,cellGap:h=3,weekStartDay:g=1,showMonthLabels:_=!0,showDayLabels:v=!0,showLegend:y=!0,weeks:b=52,ariaLabel:x=`Contribution graph`,onDayClick:S,tooltipContent:C,className:w}){let T=t.useDateTimeFormatter({month:`short`}),E=t.useDateTimeFormatter({weekday:`short`}),D=t.useDateTimeFormatter({weekday:`long`,year:`numeric`,month:`long`,day:`numeric`}),O=m+h,k=(0,n.useRef)(null),A=(0,n.useRef)(null),j=p??l,[M,N]=(0,n.useState)({col:0,row:0}),[P,F]=(0,n.useState)(null),I=(0,n.useMemo)(()=>{let e=new Map;for(let t of i)e.set(t.date,t.count);return e},[i]),L=(0,n.useMemo)(()=>Math.max(1,...i.map(e=>e.count)),[i]),{grid:R,monthLabels:z}=(0,n.useMemo)(()=>{let e=new Date;e.setHours(0,0,0,0);let t=(e.getDay()-g+7)%7,n=new Date(e);n.setDate(e.getDate()-t);let r=new Date(n);r.setDate(n.getDate()-(b-1)*7);let i=[],o=[],s=-1;for(let t=0;t<b;t++){let n=new Date(r);n.setDate(r.getDate()+t*7),n.getMonth()!==s&&(o.push({col:t,month:a(n.getMonth(),T)}),s=n.getMonth());let c=[];for(let n=0;n<7;n++){let i=new Date(r);i.setDate(r.getDate()+t*7+n);let a=i>e,o=u(i),s=a?0:I.get(o)??0,l=s===0?0:Math.min(d,Math.ceil(s/L*d));c.push({iso:o,count:s,level:l,future:a})}i.push(c)}return{grid:i,monthLabels:o}},[I,L,d,b,g,T]),B=(0,n.useMemo)(()=>[1,3,5].map(e=>({row:(e-g+7)%7,label:o(e,E)})),[g,E]),V=v?s:0,H=_?c:0,U=V+b*O-h,W=H+7*O-h,G=Math.min(4,Math.floor(m/3));function K(e,t){let n=Math.max(0,Math.min(b-1,e)),r=Math.max(0,Math.min(6,t));N({col:n,row:r}),k.current?.querySelector(`[data-col="${n}"][data-row="${r}"]`)?.focus()}function q(e,t,n){let r={ArrowRight:[t+1,n],ArrowLeft:[t-1,n],ArrowDown:[t,n+1],ArrowUp:[t,n-1]};if(r[e.key])e.preventDefault(),K(...r[e.key]);else if(e.key===`Enter`||e.key===` `){e.preventDefault();let r=R[t]?.[n];r&&!r.future&&S?.({date:r.iso,count:r.count})}}function J(e,t){let n=A.current;if(!n)return;let r=n.getBoundingClientRect(),i=e.currentTarget.getBoundingClientRect();F({x:i.left-r.left+m/2,y:i.top-r.top,text:t})}function Y(e){return e.future?j[0]:j[Math.min(e.level,j.length-1)]??j[j.length-1]}function X(e){if(e.future)return f(e.iso,D);let t=f(e.iso,D);return C?.({date:e.iso,count:e.count})??`${e.count} contribution${e.count===1?``:`s`} on ${t}`}return(0,r.jsxs)(`div`,{ref:A,className:[e.default.wrapper,w].filter(Boolean).join(` `),children:[(0,r.jsxs)(`svg`,{ref:k,width:U,height:W,className:e.default.svg,"aria-label":x,role:`img`,children:[_&&z.map(({col:t,month:n})=>(0,r.jsx)(`text`,{x:V+t*O,y:12,className:e.default.label,children:n},`m-${t}`)),v&&B.map(({row:t,label:n})=>(0,r.jsx)(`text`,{x:0,y:H+t*O+m-1,className:e.default.label,children:n},`d-${t}`)),(0,r.jsx)(`g`,{role:`grid`,"aria-label":x,children:R.map((t,n)=>(0,r.jsx)(`g`,{role:`row`,children:t.map((t,i)=>{let a=X(t),o=M.col===n&&M.row===i;return(0,r.jsx)(`rect`,{"data-col":n,"data-row":i,x:V+n*O,y:H+i*O,width:m,height:m,rx:G,fill:Y(t),opacity:t.future?.35:1,className:e.default.cell,role:`gridcell`,"aria-label":a,"aria-disabled":t.future||void 0,tabIndex:o?0:-1,onClick:()=>!t.future&&S?.({date:t.iso,count:t.count}),onKeyDown:e=>q(e,n,i),onFocus:()=>N({col:n,row:i}),onMouseEnter:e=>J(e,a),onMouseLeave:()=>F(null)},`${n}-${i}`)})},n))})]}),P&&(0,r.jsx)(`div`,{className:e.default.tooltip,style:{left:P.x,top:P.y},role:`tooltip`,children:P.text}),y&&(0,r.jsxs)(`div`,{className:e.default.legend,children:[(0,r.jsx)(`span`,{className:e.default.legendLabel,children:`Less`}),Array.from({length:d+1},(t,n)=>(0,r.jsx)(`svg`,{width:m,height:m,"aria-hidden":`true`,className:e.default.legendCell,children:(0,r.jsx)(`rect`,{width:m,height:m,rx:G,fill:j[Math.min(n,j.length-1)]})},n)),(0,r.jsx)(`span`,{className:e.default.legendLabel,children:`More`})]})]})}exports.ContributionGraph=p;
|
|
2
2
|
//# sourceMappingURL=ContributionGraph.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContributionGraph.cjs","names":[],"sources":["../../../src/components/ContributionGraph/ContributionGraph.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from \"react\";\nimport type { KeyboardEvent } from \"react\";\nimport styles from \"./ContributionGraph.module.css\";\n\nexport interface ContributionDay {\n /** ISO 8601 date — \"YYYY-MM-DD\". */\n date: string;\n /** Non-negative activity count. */\n count: number;\n}\n\nexport interface ContributionGraphProps {\n /** Activity data. Days absent from the array are treated as count = 0. */\n data: ContributionDay[];\n /**\n * Number of intensity levels (excluding 0).\n * @default 4\n */\n maxLevel?: number;\n /**\n * Colour scale — length must be maxLevel + 1 (index 0 = empty).\n * Defaults to the Adwaita green palette.\n */\n colorScale?: string[];\n /** Cell side length in pixels. @default 12 */\n cellSize?: number;\n /** Gap between cells in pixels. @default 3 */\n cellGap?: number;\n /** 0 = Sunday · 1 = Monday (GNOME locale default). @default 1 */\n weekStartDay?: 0 | 1;\n /** @default true */\n showMonthLabels?: boolean;\n /** @default true */\n showDayLabels?: boolean;\n /** @default true */\n showLegend?: boolean;\n /** Number of week columns to display. @default 52 */\n weeks?: number;\n /** @default \"Contribution graph\" */\n ariaLabel?: string;\n onDayClick?: (day: ContributionDay) => void;\n /** Returns a plain-text tooltip string for a day. */\n tooltipContent?: (day: ContributionDay) => string;\n className?: string;\n}\n\nconst SHORT_MONTHS = [\n \"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\",\n \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\",\n];\n// 0=Sun … 6=Sat\nconst SHORT_DAYS = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\n\nconst DAY_LABEL_WIDTH = 28;\nconst MONTH_LABEL_HEIGHT = 20;\n\nconst DEFAULT_COLORS = [\n \"var(--gnome-card-shade-color, rgba(0,0,0,0.07))\",\n \"var(--gnome-green-1, #8ff0a4)\",\n \"var(--gnome-green-2, #57e389)\",\n \"var(--gnome-green-4, #2ec27e)\",\n \"var(--gnome-green-5, #26a269)\",\n];\n\nfunction dateToIso(date: Date): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, \"0\");\n const d = String(date.getDate()).padStart(2, \"0\");\n return `${y}-${m}-${d}`;\n}\n\nfunction isoToLocal(iso: string): Date {\n const [y, m, d] = iso.split(\"-\").map(Number);\n return new Date(y, m - 1, d);\n}\n\nfunction fullDateLabel(iso: string): string {\n return isoToLocal(iso).toLocaleDateString(\"en-US\", {\n weekday: \"long\",\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n });\n}\n\ninterface GridCell {\n iso: string;\n count: number;\n level: number;\n future: boolean;\n}\n\nexport function ContributionGraph({\n data,\n maxLevel = 4,\n colorScale,\n cellSize = 12,\n cellGap = 3,\n weekStartDay = 1,\n showMonthLabels = true,\n showDayLabels = true,\n showLegend = true,\n weeks = 52,\n ariaLabel = \"Contribution graph\",\n onDayClick,\n tooltipContent,\n className,\n}: ContributionGraphProps) {\n const stride = cellSize + cellGap;\n const svgRef = useRef<SVGSVGElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n const colors = colorScale ?? DEFAULT_COLORS;\n\n const [focusedCell, setFocusedCell] = useState({ col: 0, row: 0 });\n const [tooltip, setTooltip] = useState<{\n x: number;\n y: number;\n text: string;\n } | null>(null);\n\n const countMap = useMemo(() => {\n const map = new Map<string, number>();\n for (const d of data) map.set(d.date, d.count);\n return map;\n }, [data]);\n\n const maxCount = useMemo(\n () => Math.max(1, ...data.map((d) => d.count)),\n [data],\n );\n\n const { grid, monthLabels } = useMemo(() => {\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n\n // Align to start of current week\n const daysFromWeekStart = (today.getDay() - weekStartDay + 7) % 7;\n const lastWeekStart = new Date(today);\n lastWeekStart.setDate(today.getDate() - daysFromWeekStart);\n\n const firstDay = new Date(lastWeekStart);\n firstDay.setDate(lastWeekStart.getDate() - (weeks - 1) * 7);\n\n const gridResult: GridCell[][] = [];\n const labels: { col: number; month: string }[] = [];\n let lastMonth = -1;\n\n for (let col = 0; col < weeks; col++) {\n const colStart = new Date(firstDay);\n colStart.setDate(firstDay.getDate() + col * 7);\n\n // Month label when the column starts a new month\n if (colStart.getMonth() !== lastMonth) {\n labels.push({ col, month: SHORT_MONTHS[colStart.getMonth()] });\n lastMonth = colStart.getMonth();\n }\n\n const column: GridCell[] = [];\n for (let row = 0; row < 7; row++) {\n const date = new Date(firstDay);\n date.setDate(firstDay.getDate() + col * 7 + row);\n const future = date > today;\n const iso = dateToIso(date);\n const count = future ? 0 : (countMap.get(iso) ?? 0);\n const level =\n count === 0\n ? 0\n : Math.min(maxLevel, Math.ceil((count / maxCount) * maxLevel));\n column.push({ iso, count, level, future });\n }\n gridResult.push(column);\n }\n\n return { grid: gridResult, monthLabels: labels };\n }, [countMap, maxCount, maxLevel, weeks, weekStartDay]);\n\n // Always label Mon(1), Wed(3), Fri(5) — rows shift with weekStartDay\n const labelRows = useMemo(\n () =>\n [1, 3, 5].map((dayIndex) => ({\n row: (dayIndex - weekStartDay + 7) % 7,\n label: SHORT_DAYS[dayIndex],\n })),\n [weekStartDay],\n );\n\n const dayLabelW = showDayLabels ? DAY_LABEL_WIDTH : 0;\n const monthLabelH = showMonthLabels ? MONTH_LABEL_HEIGHT : 0;\n const svgWidth = dayLabelW + weeks * stride - cellGap;\n const svgHeight = monthLabelH + 7 * stride - cellGap;\n const cellRx = Math.min(4, Math.floor(cellSize / 3));\n\n function focusCell(col: number, row: number) {\n const c = Math.max(0, Math.min(weeks - 1, col));\n const r = Math.max(0, Math.min(6, row));\n setFocusedCell({ col: c, row: r });\n svgRef.current\n ?.querySelector<SVGRectElement>(`[data-col=\"${c}\"][data-row=\"${r}\"]`)\n ?.focus();\n }\n\n function handleCellKey(e: KeyboardEvent, col: number, row: number) {\n const moves: Record<string, [number, number]> = {\n ArrowRight: [col + 1, row],\n ArrowLeft: [col - 1, row],\n ArrowDown: [col, row + 1],\n ArrowUp: [col, row - 1],\n };\n if (moves[e.key]) {\n e.preventDefault();\n focusCell(...moves[e.key]);\n } else if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n const cell = grid[col]?.[row];\n if (cell && !cell.future) onDayClick?.({ date: cell.iso, count: cell.count });\n }\n }\n\n function handleMouseEnter(\n e: React.MouseEvent<SVGRectElement>,\n text: string,\n ) {\n const wrapper = wrapperRef.current;\n if (!wrapper) return;\n const wRect = wrapper.getBoundingClientRect();\n const rRect = (e.currentTarget as Element).getBoundingClientRect();\n setTooltip({\n x: rRect.left - wRect.left + cellSize / 2,\n y: rRect.top - wRect.top,\n text,\n });\n }\n\n function cellColor(cell: GridCell): string {\n if (cell.future) return colors[0];\n return colors[Math.min(cell.level, colors.length - 1)] ?? colors[colors.length - 1];\n }\n\n function cellTooltip(cell: GridCell): string {\n if (cell.future) return fullDateLabel(cell.iso);\n return (\n tooltipContent?.({ date: cell.iso, count: cell.count }) ??\n `${cell.count} contribution${cell.count !== 1 ? \"s\" : \"\"} on ${fullDateLabel(cell.iso)}`\n );\n }\n\n return (\n <div\n ref={wrapperRef}\n className={[styles.wrapper, className].filter(Boolean).join(\" \")}\n >\n <svg\n ref={svgRef}\n width={svgWidth}\n height={svgHeight}\n className={styles.svg}\n aria-label={ariaLabel}\n role=\"img\"\n >\n {/* Month labels */}\n {showMonthLabels &&\n monthLabels.map(({ col, month }) => (\n <text\n key={`m-${col}`}\n x={dayLabelW + col * stride}\n y={12}\n className={styles.label}\n >\n {month}\n </text>\n ))}\n\n {/* Day-of-week labels: Mon, Wed, Fri */}\n {showDayLabels &&\n labelRows.map(({ row, label }) => (\n <text\n key={`d-${row}`}\n x={0}\n y={monthLabelH + row * stride + cellSize - 1}\n className={styles.label}\n >\n {label}\n </text>\n ))}\n\n {/* Activity grid */}\n <g role=\"grid\" aria-label={ariaLabel}>\n {grid.map((column, col) => (\n <g key={col} role=\"row\">\n {column.map((cell, row) => {\n const tipText = cellTooltip(cell);\n const isFocused =\n focusedCell.col === col && focusedCell.row === row;\n\n return (\n <rect\n key={`${col}-${row}`}\n data-col={col}\n data-row={row}\n x={dayLabelW + col * stride}\n y={monthLabelH + row * stride}\n width={cellSize}\n height={cellSize}\n rx={cellRx}\n fill={cellColor(cell)}\n opacity={cell.future ? 0.35 : 1}\n className={styles.cell}\n role=\"gridcell\"\n aria-label={tipText}\n aria-disabled={cell.future || undefined}\n tabIndex={isFocused ? 0 : -1}\n onClick={() => !cell.future && onDayClick?.({ date: cell.iso, count: cell.count })}\n onKeyDown={(e) => handleCellKey(e, col, row)}\n onFocus={() => setFocusedCell({ col, row })}\n onMouseEnter={(e) => handleMouseEnter(e, tipText)}\n onMouseLeave={() => setTooltip(null)}\n />\n );\n })}\n </g>\n ))}\n </g>\n </svg>\n\n {tooltip && (\n <div\n className={styles.tooltip}\n style={{ left: tooltip.x, top: tooltip.y }}\n role=\"tooltip\"\n >\n {tooltip.text}\n </div>\n )}\n\n {showLegend && (\n <div className={styles.legend}>\n <span className={styles.legendLabel}>Less</span>\n {Array.from({ length: maxLevel + 1 }, (_, i) => (\n <svg\n key={i}\n width={cellSize}\n height={cellSize}\n aria-hidden=\"true\"\n className={styles.legendCell}\n >\n <rect\n width={cellSize}\n height={cellSize}\n rx={cellRx}\n fill={colors[Math.min(i, colors.length - 1)]}\n />\n </svg>\n ))}\n <span className={styles.legendLabel}>More</span>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"4GA8CA,IAAM,EAAe,CACnB,MAAO,MAAO,MAAO,MAAO,MAAO,MACnC,MAAO,MAAO,MAAO,MAAO,MAAO,MACpC,CAEK,EAAa,CAAC,MAAO,MAAO,MAAO,MAAO,MAAO,MAAO,MAAM,CAE9D,EAAkB,GAClB,EAAqB,GAErB,EAAiB,CACrB,kDACA,gCACA,gCACA,gCACA,gCACD,CAED,SAAS,EAAU,EAAoB,CAIrC,MAAO,GAHG,EAAK,aAAa,CAGhB,GAFF,OAAO,EAAK,UAAU,CAAG,EAAE,CAAC,SAAS,EAAG,IAAI,CAErC,GADP,OAAO,EAAK,SAAS,CAAC,CAAC,SAAS,EAAG,IAAI,GAInD,SAAS,EAAW,EAAmB,CACrC,GAAM,CAAC,EAAG,EAAG,GAAK,EAAI,MAAM,IAAI,CAAC,IAAI,OAAO,CAC5C,OAAO,IAAI,KAAK,EAAG,EAAI,EAAG,EAAE,CAG9B,SAAS,EAAc,EAAqB,CAC1C,OAAO,EAAW,EAAI,CAAC,mBAAmB,QAAS,CACjD,QAAS,OACT,KAAM,UACN,MAAO,OACP,IAAK,UACN,CAAC,CAUJ,SAAgB,EAAkB,CAChC,OACA,WAAW,EACX,aACA,WAAW,GACX,UAAU,EACV,eAAe,EACf,kBAAkB,GAClB,gBAAgB,GAChB,aAAa,GACb,QAAQ,GACR,YAAY,qBACZ,aACA,iBACA,aACyB,CACzB,IAAM,EAAS,EAAW,EACpB,GAAA,EAAA,EAAA,QAA+B,KAAK,CACpC,GAAA,EAAA,EAAA,QAAoC,KAAK,CACzC,EAAS,GAAc,EAEvB,CAAC,EAAa,IAAA,EAAA,EAAA,UAA2B,CAAE,IAAK,EAAG,IAAK,EAAG,CAAC,CAC5D,CAAC,EAAS,IAAA,EAAA,EAAA,UAIN,KAAK,CAET,GAAA,EAAA,EAAA,aAAyB,CAC7B,IAAM,EAAM,IAAI,IAChB,IAAK,IAAM,KAAK,EAAM,EAAI,IAAI,EAAE,KAAM,EAAE,MAAM,CAC9C,OAAO,GACN,CAAC,EAAK,CAAC,CAEJ,GAAA,EAAA,EAAA,aACE,KAAK,IAAI,EAAG,GAAG,EAAK,IAAK,GAAM,EAAE,MAAM,CAAC,CAC9C,CAAC,EAAK,CACP,CAEK,CAAE,OAAM,gBAAA,EAAA,EAAA,aAA8B,CAC1C,IAAM,EAAQ,IAAI,KAClB,EAAM,SAAS,EAAG,EAAG,EAAG,EAAE,CAG1B,IAAM,GAAqB,EAAM,QAAQ,CAAG,EAAe,GAAK,EAC1D,EAAgB,IAAI,KAAK,EAAM,CACrC,EAAc,QAAQ,EAAM,SAAS,CAAG,EAAkB,CAE1D,IAAM,EAAW,IAAI,KAAK,EAAc,CACxC,EAAS,QAAQ,EAAc,SAAS,EAAI,EAAQ,GAAK,EAAE,CAE3D,IAAM,EAA2B,EAAE,CAC7B,EAA2C,EAAE,CAC/C,EAAY,GAEhB,IAAK,IAAI,EAAM,EAAG,EAAM,EAAO,IAAO,CACpC,IAAM,EAAW,IAAI,KAAK,EAAS,CACnC,EAAS,QAAQ,EAAS,SAAS,CAAG,EAAM,EAAE,CAG1C,EAAS,UAAU,GAAK,IAC1B,EAAO,KAAK,CAAE,MAAK,MAAO,EAAa,EAAS,UAAU,EAAG,CAAC,CAC9D,EAAY,EAAS,UAAU,EAGjC,IAAM,EAAqB,EAAE,CAC7B,IAAK,IAAI,EAAM,EAAG,EAAM,EAAG,IAAO,CAChC,IAAM,EAAO,IAAI,KAAK,EAAS,CAC/B,EAAK,QAAQ,EAAS,SAAS,CAAG,EAAM,EAAI,EAAI,CAChD,IAAM,EAAS,EAAO,EAChB,EAAM,EAAU,EAAK,CACrB,EAAQ,EAAS,EAAK,EAAS,IAAI,EAAI,EAAI,EAC3C,EACJ,IAAU,EACN,EACA,KAAK,IAAI,EAAU,KAAK,KAAM,EAAQ,EAAY,EAAS,CAAC,CAClE,EAAO,KAAK,CAAE,MAAK,QAAO,QAAO,SAAQ,CAAC,CAE5C,EAAW,KAAK,EAAO,CAGzB,MAAO,CAAE,KAAM,EAAY,YAAa,EAAQ,EAC/C,CAAC,EAAU,EAAU,EAAU,EAAO,EAAa,CAAC,CAGjD,GAAA,EAAA,EAAA,aAEF,CAAC,EAAG,EAAG,EAAE,CAAC,IAAK,IAAc,CAC3B,KAAM,EAAW,EAAe,GAAK,EACrC,MAAO,EAAW,GACnB,EAAE,CACL,CAAC,EAAa,CACf,CAEK,EAAY,EAAgB,EAAkB,EAC9C,EAAc,EAAkB,EAAqB,EACrD,EAAW,EAAY,EAAQ,EAAS,EACxC,EAAY,EAAc,EAAI,EAAS,EACvC,EAAS,KAAK,IAAI,EAAG,KAAK,MAAM,EAAW,EAAE,CAAC,CAEpD,SAAS,EAAU,EAAa,EAAa,CAC3C,IAAM,EAAI,KAAK,IAAI,EAAG,KAAK,IAAI,EAAQ,EAAG,EAAI,CAAC,CACzC,EAAI,KAAK,IAAI,EAAG,KAAK,IAAI,EAAG,EAAI,CAAC,CACvC,EAAe,CAAE,IAAK,EAAG,IAAK,EAAG,CAAC,CAClC,EAAO,SACH,cAA8B,cAAc,EAAE,eAAe,EAAE,IAAI,EACnE,OAAO,CAGb,SAAS,EAAc,EAAkB,EAAa,EAAa,CACjE,IAAM,EAA0C,CAC9C,WAAY,CAAC,EAAM,EAAG,EAAI,CAC1B,UAAW,CAAC,EAAM,EAAG,EAAI,CACzB,UAAW,CAAC,EAAK,EAAM,EAAE,CACzB,QAAS,CAAC,EAAK,EAAM,EAAE,CACxB,CACD,GAAI,EAAM,EAAE,KACV,EAAE,gBAAgB,CAClB,EAAU,GAAG,EAAM,EAAE,KAAK,SACjB,EAAE,MAAQ,SAAW,EAAE,MAAQ,IAAK,CAC7C,EAAE,gBAAgB,CAClB,IAAM,EAAO,EAAK,KAAO,GACrB,GAAQ,CAAC,EAAK,QAAQ,IAAa,CAAE,KAAM,EAAK,IAAK,MAAO,EAAK,MAAO,CAAC,EAIjF,SAAS,EACP,EACA,EACA,CACA,IAAM,EAAU,EAAW,QAC3B,GAAI,CAAC,EAAS,OACd,IAAM,EAAQ,EAAQ,uBAAuB,CACvC,EAAS,EAAE,cAA0B,uBAAuB,CAClE,EAAW,CACT,EAAG,EAAM,KAAO,EAAM,KAAO,EAAW,EACxC,EAAG,EAAM,IAAM,EAAM,IACrB,OACD,CAAC,CAGJ,SAAS,EAAU,EAAwB,CAEzC,OADI,EAAK,OAAe,EAAO,GACxB,EAAO,KAAK,IAAI,EAAK,MAAO,EAAO,OAAS,EAAE,GAAK,EAAO,EAAO,OAAS,GAGnF,SAAS,EAAY,EAAwB,CAE3C,OADI,EAAK,OAAe,EAAc,EAAK,IAAI,CAE7C,IAAiB,CAAE,KAAM,EAAK,IAAK,MAAO,EAAK,MAAO,CAAC,EACvD,GAAG,EAAK,MAAM,eAAe,EAAK,QAAU,EAAU,GAAN,IAAS,MAAM,EAAc,EAAK,IAAI,GAI1F,OACE,EAAA,EAAA,MAAC,MAAD,CACE,IAAK,EACL,UAAW,CAAC,EAAA,QAAO,QAAS,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI,UAFlE,EAIE,EAAA,EAAA,MAAC,MAAD,CACE,IAAK,EACL,MAAO,EACP,OAAQ,EACR,UAAW,EAAA,QAAO,IAClB,aAAY,EACZ,KAAK,eANP,CASG,GACC,EAAY,KAAK,CAAE,MAAK,YACtB,EAAA,EAAA,KAAC,OAAD,CAEE,EAAG,EAAY,EAAM,EACrB,EAAG,GACH,UAAW,EAAA,QAAO,eAEjB,EACI,CANA,KAAK,IAML,CACP,CAGH,GACC,EAAU,KAAK,CAAE,MAAK,YACpB,EAAA,EAAA,KAAC,OAAD,CAEE,EAAG,EACH,EAAG,EAAc,EAAM,EAAS,EAAW,EAC3C,UAAW,EAAA,QAAO,eAEjB,EACI,CANA,KAAK,IAML,CACP,EAGJ,EAAA,EAAA,KAAC,IAAD,CAAG,KAAK,OAAO,aAAY,WACxB,EAAK,KAAK,EAAQ,KACjB,EAAA,EAAA,KAAC,IAAD,CAAa,KAAK,eACf,EAAO,KAAK,EAAM,IAAQ,CACzB,IAAM,EAAU,EAAY,EAAK,CAC3B,EACJ,EAAY,MAAQ,GAAO,EAAY,MAAQ,EAEjD,OACE,EAAA,EAAA,KAAC,OAAD,CAEE,WAAU,EACV,WAAU,EACV,EAAG,EAAY,EAAM,EACrB,EAAG,EAAc,EAAM,EACvB,MAAO,EACP,OAAQ,EACR,GAAI,EACJ,KAAM,EAAU,EAAK,CACrB,QAAS,EAAK,OAAS,IAAO,EAC9B,UAAW,EAAA,QAAO,KAClB,KAAK,WACL,aAAY,EACZ,gBAAe,EAAK,QAAU,IAAA,GAC9B,SAAU,EAAY,EAAI,GAC1B,YAAe,CAAC,EAAK,QAAU,IAAa,CAAE,KAAM,EAAK,IAAK,MAAO,EAAK,MAAO,CAAC,CAClF,UAAY,GAAM,EAAc,EAAG,EAAK,EAAI,CAC5C,YAAe,EAAe,CAAE,MAAK,MAAK,CAAC,CAC3C,aAAe,GAAM,EAAiB,EAAG,EAAQ,CACjD,iBAAoB,EAAW,KAAK,CACpC,CApBK,GAAG,EAAI,GAAG,IAoBf,EAEJ,CACA,CA/BI,EA+BJ,CACJ,CACA,CAAA,CACA,GAEL,IACC,EAAA,EAAA,KAAC,MAAD,CACE,UAAW,EAAA,QAAO,QAClB,MAAO,CAAE,KAAM,EAAQ,EAAG,IAAK,EAAQ,EAAG,CAC1C,KAAK,mBAEJ,EAAQ,KACL,CAAA,CAGP,IACC,EAAA,EAAA,MAAC,MAAD,CAAK,UAAW,EAAA,QAAO,gBAAvB,EACE,EAAA,EAAA,KAAC,OAAD,CAAM,UAAW,EAAA,QAAO,qBAAa,OAAW,CAAA,CAC/C,MAAM,KAAK,CAAE,OAAQ,EAAW,EAAG,EAAG,EAAG,KACxC,EAAA,EAAA,KAAC,MAAD,CAEE,MAAO,EACP,OAAQ,EACR,cAAY,OACZ,UAAW,EAAA,QAAO,qBAElB,EAAA,EAAA,KAAC,OAAD,CACE,MAAO,EACP,OAAQ,EACR,GAAI,EACJ,KAAM,EAAO,KAAK,IAAI,EAAG,EAAO,OAAS,EAAE,EAC3C,CAAA,CACE,CAZC,EAYD,CACN,EACF,EAAA,EAAA,KAAC,OAAD,CAAM,UAAW,EAAA,QAAO,qBAAa,OAAW,CAAA,CAC5C,GAEJ"}
|
|
1
|
+
{"version":3,"file":"ContributionGraph.cjs","names":[],"sources":["../../../src/components/ContributionGraph/ContributionGraph.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from \"react\";\nimport type { KeyboardEvent } from \"react\";\nimport styles from \"./ContributionGraph.module.css\";\nimport { useDateTimeFormatter } from \"../GnomeProvider/GnomeContext\";\n\nexport interface ContributionDay {\n /** ISO 8601 date — \"YYYY-MM-DD\". */\n date: string;\n /** Non-negative activity count. */\n count: number;\n}\n\nexport interface ContributionGraphProps {\n /** Activity data. Days absent from the array are treated as count = 0. */\n data: ContributionDay[];\n /**\n * Number of intensity levels (excluding 0).\n * @default 4\n */\n maxLevel?: number;\n /**\n * Colour scale — length must be maxLevel + 1 (index 0 = empty).\n * Defaults to the Adwaita green palette.\n */\n colorScale?: string[];\n /** Cell side length in pixels. @default 12 */\n cellSize?: number;\n /** Gap between cells in pixels. @default 3 */\n cellGap?: number;\n /** 0 = Sunday · 1 = Monday (GNOME locale default). @default 1 */\n weekStartDay?: 0 | 1;\n /** @default true */\n showMonthLabels?: boolean;\n /** @default true */\n showDayLabels?: boolean;\n /** @default true */\n showLegend?: boolean;\n /** Number of week columns to display. @default 52 */\n weeks?: number;\n /** @default \"Contribution graph\" */\n ariaLabel?: string;\n onDayClick?: (day: ContributionDay) => void;\n /** Returns a plain-text tooltip string for a day. */\n tooltipContent?: (day: ContributionDay) => string;\n className?: string;\n}\n\n// Reference Sunday: 2000-01-02 is a Sunday (day index 0)\nconst REF_SUNDAY = new Date(2000, 0, 2);\n\nfunction getShortMonth(\n monthIndex: number,\n formatter: Intl.DateTimeFormat,\n): string {\n return formatter.format(new Date(2000, monthIndex));\n}\n\nfunction getShortDay(\n dayIndex: number,\n formatter: Intl.DateTimeFormat,\n): string {\n const date = new Date(REF_SUNDAY);\n date.setDate(REF_SUNDAY.getDate() + dayIndex);\n return formatter.format(date);\n}\n\nconst DAY_LABEL_WIDTH = 28;\nconst MONTH_LABEL_HEIGHT = 20;\n\nconst DEFAULT_COLORS = [\n \"var(--gnome-card-shade-color, rgba(0,0,0,0.07))\",\n \"var(--gnome-green-1, #8ff0a4)\",\n \"var(--gnome-green-2, #57e389)\",\n \"var(--gnome-green-4, #2ec27e)\",\n \"var(--gnome-green-5, #26a269)\",\n];\n\nfunction dateToIso(date: Date): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, \"0\");\n const d = String(date.getDate()).padStart(2, \"0\");\n return `${y}-${m}-${d}`;\n}\n\nfunction isoToLocal(iso: string): Date {\n const [y, m, d] = iso.split(\"-\").map(Number);\n return new Date(y, m - 1, d);\n}\n\nfunction fullDateLabel(iso: string, formatter: Intl.DateTimeFormat): string {\n return formatter.format(isoToLocal(iso));\n}\n\ninterface GridCell {\n iso: string;\n count: number;\n level: number;\n future: boolean;\n}\n\nexport function ContributionGraph({\n data,\n maxLevel = 4,\n colorScale,\n cellSize = 12,\n cellGap = 3,\n weekStartDay = 1,\n showMonthLabels = true,\n showDayLabels = true,\n showLegend = true,\n weeks = 52,\n ariaLabel = \"Contribution graph\",\n onDayClick,\n tooltipContent,\n className,\n}: ContributionGraphProps) {\n const monthFormatter = useDateTimeFormatter({ month: \"short\" });\n const weekdayFormatter = useDateTimeFormatter({ weekday: \"short\" });\n const fullDateFormatter = useDateTimeFormatter({\n weekday: \"long\",\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n });\n const stride = cellSize + cellGap;\n const svgRef = useRef<SVGSVGElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n const colors = colorScale ?? DEFAULT_COLORS;\n\n const [focusedCell, setFocusedCell] = useState({ col: 0, row: 0 });\n const [tooltip, setTooltip] = useState<{\n x: number;\n y: number;\n text: string;\n } | null>(null);\n\n const countMap = useMemo(() => {\n const map = new Map<string, number>();\n for (const d of data) map.set(d.date, d.count);\n return map;\n }, [data]);\n\n const maxCount = useMemo(\n () => Math.max(1, ...data.map((d) => d.count)),\n [data],\n );\n\n const { grid, monthLabels } = useMemo(() => {\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n\n // Align to start of current week\n const daysFromWeekStart = (today.getDay() - weekStartDay + 7) % 7;\n const lastWeekStart = new Date(today);\n lastWeekStart.setDate(today.getDate() - daysFromWeekStart);\n\n const firstDay = new Date(lastWeekStart);\n firstDay.setDate(lastWeekStart.getDate() - (weeks - 1) * 7);\n\n const gridResult: GridCell[][] = [];\n const labels: { col: number; month: string }[] = [];\n let lastMonth = -1;\n\n for (let col = 0; col < weeks; col++) {\n const colStart = new Date(firstDay);\n colStart.setDate(firstDay.getDate() + col * 7);\n\n // Month label when the column starts a new month\n if (colStart.getMonth() !== lastMonth) {\n labels.push({\n col,\n month: getShortMonth(colStart.getMonth(), monthFormatter),\n });\n lastMonth = colStart.getMonth();\n }\n\n const column: GridCell[] = [];\n for (let row = 0; row < 7; row++) {\n const date = new Date(firstDay);\n date.setDate(firstDay.getDate() + col * 7 + row);\n const future = date > today;\n const iso = dateToIso(date);\n const count = future ? 0 : (countMap.get(iso) ?? 0);\n const level =\n count === 0\n ? 0\n : Math.min(maxLevel, Math.ceil((count / maxCount) * maxLevel));\n column.push({ iso, count, level, future });\n }\n gridResult.push(column);\n }\n\n return { grid: gridResult, monthLabels: labels };\n }, [countMap, maxCount, maxLevel, weeks, weekStartDay, monthFormatter]);\n\n // Always label Mon(1), Wed(3), Fri(5) — rows shift with weekStartDay\n const labelRows = useMemo(\n () =>\n [1, 3, 5].map((dayIndex) => ({\n row: (dayIndex - weekStartDay + 7) % 7,\n label: getShortDay(dayIndex, weekdayFormatter),\n })),\n [weekStartDay, weekdayFormatter],\n );\n\n const dayLabelW = showDayLabels ? DAY_LABEL_WIDTH : 0;\n const monthLabelH = showMonthLabels ? MONTH_LABEL_HEIGHT : 0;\n const svgWidth = dayLabelW + weeks * stride - cellGap;\n const svgHeight = monthLabelH + 7 * stride - cellGap;\n const cellRx = Math.min(4, Math.floor(cellSize / 3));\n\n function focusCell(col: number, row: number) {\n const c = Math.max(0, Math.min(weeks - 1, col));\n const r = Math.max(0, Math.min(6, row));\n setFocusedCell({ col: c, row: r });\n svgRef.current\n ?.querySelector<SVGRectElement>(`[data-col=\"${c}\"][data-row=\"${r}\"]`)\n ?.focus();\n }\n\n function handleCellKey(e: KeyboardEvent, col: number, row: number) {\n const moves: Record<string, [number, number]> = {\n ArrowRight: [col + 1, row],\n ArrowLeft: [col - 1, row],\n ArrowDown: [col, row + 1],\n ArrowUp: [col, row - 1],\n };\n if (moves[e.key]) {\n e.preventDefault();\n focusCell(...moves[e.key]);\n } else if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n const cell = grid[col]?.[row];\n if (cell && !cell.future) onDayClick?.({ date: cell.iso, count: cell.count });\n }\n }\n\n function handleMouseEnter(\n e: React.MouseEvent<SVGRectElement>,\n text: string,\n ) {\n const wrapper = wrapperRef.current;\n if (!wrapper) return;\n const wRect = wrapper.getBoundingClientRect();\n const rRect = (e.currentTarget as Element).getBoundingClientRect();\n setTooltip({\n x: rRect.left - wRect.left + cellSize / 2,\n y: rRect.top - wRect.top,\n text,\n });\n }\n\n function cellColor(cell: GridCell): string {\n if (cell.future) return colors[0];\n return colors[Math.min(cell.level, colors.length - 1)] ?? colors[colors.length - 1];\n }\n\n function cellTooltip(cell: GridCell): string {\n if (cell.future) return fullDateLabel(cell.iso, fullDateFormatter);\n const formattedDate = fullDateLabel(cell.iso, fullDateFormatter);\n return (\n tooltipContent?.({ date: cell.iso, count: cell.count }) ??\n `${cell.count} contribution${cell.count !== 1 ? \"s\" : \"\"} on ${formattedDate}`\n );\n }\n\n return (\n <div\n ref={wrapperRef}\n className={[styles.wrapper, className].filter(Boolean).join(\" \")}\n >\n <svg\n ref={svgRef}\n width={svgWidth}\n height={svgHeight}\n className={styles.svg}\n aria-label={ariaLabel}\n role=\"img\"\n >\n {/* Month labels */}\n {showMonthLabels &&\n monthLabels.map(({ col, month }) => (\n <text\n key={`m-${col}`}\n x={dayLabelW + col * stride}\n y={12}\n className={styles.label}\n >\n {month}\n </text>\n ))}\n\n {/* Day-of-week labels: Mon, Wed, Fri */}\n {showDayLabels &&\n labelRows.map(({ row, label }) => (\n <text\n key={`d-${row}`}\n x={0}\n y={monthLabelH + row * stride + cellSize - 1}\n className={styles.label}\n >\n {label}\n </text>\n ))}\n\n {/* Activity grid */}\n <g role=\"grid\" aria-label={ariaLabel}>\n {grid.map((column, col) => (\n <g key={col} role=\"row\">\n {column.map((cell, row) => {\n const tipText = cellTooltip(cell);\n const isFocused =\n focusedCell.col === col && focusedCell.row === row;\n\n return (\n <rect\n key={`${col}-${row}`}\n data-col={col}\n data-row={row}\n x={dayLabelW + col * stride}\n y={monthLabelH + row * stride}\n width={cellSize}\n height={cellSize}\n rx={cellRx}\n fill={cellColor(cell)}\n opacity={cell.future ? 0.35 : 1}\n className={styles.cell}\n role=\"gridcell\"\n aria-label={tipText}\n aria-disabled={cell.future || undefined}\n tabIndex={isFocused ? 0 : -1}\n onClick={() => !cell.future && onDayClick?.({ date: cell.iso, count: cell.count })}\n onKeyDown={(e) => handleCellKey(e, col, row)}\n onFocus={() => setFocusedCell({ col, row })}\n onMouseEnter={(e) => handleMouseEnter(e, tipText)}\n onMouseLeave={() => setTooltip(null)}\n />\n );\n })}\n </g>\n ))}\n </g>\n </svg>\n\n {tooltip && (\n <div\n className={styles.tooltip}\n style={{ left: tooltip.x, top: tooltip.y }}\n role=\"tooltip\"\n >\n {tooltip.text}\n </div>\n )}\n\n {showLegend && (\n <div className={styles.legend}>\n <span className={styles.legendLabel}>Less</span>\n {Array.from({ length: maxLevel + 1 }, (_, i) => (\n <svg\n key={i}\n width={cellSize}\n height={cellSize}\n aria-hidden=\"true\"\n className={styles.legendCell}\n >\n <rect\n width={cellSize}\n height={cellSize}\n rx={cellRx}\n fill={colors[Math.min(i, colors.length - 1)]}\n />\n </svg>\n ))}\n <span className={styles.legendLabel}>More</span>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|