@stfrigerio/sito-template 0.1.6 → 0.1.8

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/dist/index.js CHANGED
@@ -1199,7 +1199,8 @@ const EditFAB = ({ canEdit, isEditMode, hasUnsavedChanges = false, isSaving = fa
1199
1199
 
1200
1200
  var styles$a = {"searchContainer":"SearchBar-module_searchContainer__TdM1w","searchInputWrapper":"SearchBar-module_searchInputWrapper__kCZLU","searchIcon":"SearchBar-module_searchIcon__IIxEu","searchInput":"SearchBar-module_searchInput__V4gkE","clearButton":"SearchBar-module_clearButton__7fNIY","filterSelect":"SearchBar-module_filterSelect__xIVE4","resultsDropdown":"SearchBar-module_resultsDropdown__yh6NF","loadingState":"SearchBar-module_loadingState__4gidK","emptyState":"SearchBar-module_emptyState__RbI4s","spinner":"SearchBar-module_spinner__PMc6-","resultsGroups":"SearchBar-module_resultsGroups__U24DC","resultGroup":"SearchBar-module_resultGroup__SoTQH","groupHeader":"SearchBar-module_groupHeader__bFRHA","groupIcon":"SearchBar-module_groupIcon__9ENM-","groupTitle":"SearchBar-module_groupTitle__ZekZs","groupCount":"SearchBar-module_groupCount__PQIqw","groupResults":"SearchBar-module_groupResults__xTF52","resultItem":"SearchBar-module_resultItem__VaKKy","highlighted":"SearchBar-module_highlighted__Q-3sH","resultTitle":"SearchBar-module_resultTitle__i1uqL","resultSubtitle":"SearchBar-module_resultSubtitle__LQOJ1","resultMeta":"SearchBar-module_resultMeta__Kmkrn","resultContent":"SearchBar-module_resultContent__TzVzL","highlight":"SearchBar-module_highlight__Q3PSP"};
1201
1201
 
1202
- const filterOptions = [
1202
+ // Default filter options for backwards compatibility
1203
+ const defaultFilterOptions = [
1203
1204
  { value: 'all', label: 'All', icon: FiSearch },
1204
1205
  { value: 'projects', label: 'Projects', icon: FiFolder },
1205
1206
  { value: 'clients', label: 'Clients', icon: FiUsers },
@@ -1207,16 +1208,18 @@ const filterOptions = [
1207
1208
  { value: 'interactions', label: 'Interactions', icon: FiMessageSquare },
1208
1209
  { value: 'team', label: 'Team', icon: FiUserPlus },
1209
1210
  ];
1210
- const entityIcons = {
1211
+ const defaultEntityIcons = {
1211
1212
  projects: FiFolder,
1212
1213
  clients: FiUsers,
1213
1214
  contacts: FiBook,
1214
1215
  interactions: FiMessageSquare,
1215
1216
  team: FiUserPlus,
1216
1217
  };
1217
- const SearchBar = ({ className, placeholder = "Search (Ctrl+Space)...", onSearch, onResultClick, onClear, debounceDelay = 300, minSearchLength = 2, showFilter = true, enableKeyboardShortcut = true }) => {
1218
+ const SearchBar = ({ className, placeholder = "Search (Ctrl+Space)...", onSearch, onResultClick, onClear, debounceDelay = 300, minSearchLength = 2, showFilter = true, enableKeyboardShortcut = true, filterOptions: customFilterOptions, entityIcons: customEntityIcons }) => {
1219
+ const filterOptions = customFilterOptions ?? defaultFilterOptions;
1220
+ const entityIcons = customEntityIcons ?? defaultEntityIcons;
1218
1221
  const [query, setQuery] = React.useState('');
1219
- const [filter, setFilter] = React.useState('all');
1222
+ const [filter, setFilter] = React.useState(filterOptions[0]?.value ?? 'all');
1220
1223
  const [results, setResults] = React.useState([]);
1221
1224
  const [isLoading, setIsLoading] = React.useState(false);
1222
1225
  const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
@@ -1500,9 +1503,19 @@ const useTheme = () => {
1500
1503
 
1501
1504
  var styles$7 = {"button":"ThemeSwitcher-module_button__VfRjU","iconWrapper":"ThemeSwitcher-module_iconWrapper__FpHo8","label":"ThemeSwitcher-module_label__2Hfkp","toggle":"ThemeSwitcher-module_toggle__ATXx4","toggleTrack":"ThemeSwitcher-module_toggleTrack__x28Rv","toggleThumb":"ThemeSwitcher-module_toggleThumb__V8QeN","dropdown":"ThemeSwitcher-module_dropdown__3qLdt","dropdownTrigger":"ThemeSwitcher-module_dropdownTrigger__UzYV5","dropdownMenu":"ThemeSwitcher-module_dropdownMenu__3L5hT","dropdownItem":"ThemeSwitcher-module_dropdownItem__inw-K","active":"ThemeSwitcher-module_active__OHP19","icon":"ThemeSwitcher-module_icon__iRZiJ","text":"ThemeSwitcher-module_text__OCOoA"};
1502
1505
 
1503
- const ThemeSwitcher = ({ variant = 'button', showLabel = false, className = '', }) => {
1504
- const { theme, setTheme } = useTheme();
1505
- const themes = [
1506
+ const ThemeSwitcher = ({ variant = 'button', showLabel = false, className = '', currentTheme, onThemeChange, themes: customThemes, }) => {
1507
+ // Try to use internal context if available, otherwise use props
1508
+ const contextTheme = (() => {
1509
+ try {
1510
+ return useTheme();
1511
+ }
1512
+ catch {
1513
+ return null;
1514
+ }
1515
+ })();
1516
+ const theme = currentTheme ?? contextTheme?.theme ?? 'light';
1517
+ const setTheme = onThemeChange ?? contextTheme?.setTheme ?? (() => { });
1518
+ const defaultThemes = [
1506
1519
  { value: 'light', label: 'Light', icon: jsxRuntime.jsx(FiSun, {}) },
1507
1520
  { value: 'dark', label: 'Dark', icon: jsxRuntime.jsx(FiMoon, {}) },
1508
1521
  { value: 'lossito', label: 'Lossito Light', icon: '✨' },
@@ -1510,21 +1523,22 @@ const ThemeSwitcher = ({ variant = 'button', showLabel = false, className = '',
1510
1523
  { value: 'dmood', label: 'Dmood Light', icon: '💙' },
1511
1524
  { value: 'dmood-dark', label: 'Dmood Dark', icon: '🌌' },
1512
1525
  ];
1526
+ const themes = customThemes ?? defaultThemes;
1513
1527
  const currentThemeIndex = themes.findIndex(t => t.value === theme);
1514
- const currentTheme = themes[currentThemeIndex];
1528
+ const currentThemeData = themes[currentThemeIndex] ?? themes[0];
1515
1529
  if (variant === 'toggle') {
1516
1530
  // Simple toggle between light and dark
1517
1531
  const isDark = theme.includes('dark');
1518
1532
  return (jsxRuntime.jsxs(framerMotion.motion.button, { className: `${styles$7.toggle} ${className}`, onClick: () => setTheme(isDark ? 'light' : 'dark'), whileTap: { scale: 0.95 }, "aria-label": "Toggle theme", children: [jsxRuntime.jsx(framerMotion.motion.div, { className: styles$7.toggleTrack, animate: { backgroundColor: isDark ? 'var(--color-primary)' : 'var(--color-border)' }, children: jsxRuntime.jsx(framerMotion.motion.div, { className: styles$7.toggleThumb, animate: { x: isDark ? 24 : 0 }, transition: { type: 'spring', stiffness: 500, damping: 30 }, children: isDark ? jsxRuntime.jsx(FiMoon, { size: 14 }) : jsxRuntime.jsx(FiSun, { size: 14 }) }) }), showLabel && jsxRuntime.jsx("span", { className: styles$7.label, children: isDark ? 'Dark' : 'Light' })] }));
1519
1533
  }
1520
1534
  if (variant === 'dropdown') {
1521
- return (jsxRuntime.jsxs("div", { className: `${styles$7.dropdown} ${className}`, children: [jsxRuntime.jsxs(framerMotion.motion.button, { className: styles$7.dropdownTrigger, whileTap: { scale: 0.98 }, children: [currentTheme.icon, showLabel && jsxRuntime.jsx("span", { className: styles$7.label, children: currentTheme.label })] }), jsxRuntime.jsx(framerMotion.motion.div, { className: styles$7.dropdownMenu, initial: { opacity: 0, y: -10 }, animate: { opacity: 1, y: 0 }, children: themes.map((t) => (jsxRuntime.jsxs(framerMotion.motion.button, { className: `${styles$7.dropdownItem} ${theme === t.value ? styles$7.active : ''}`, onClick: () => setTheme(t.value), whileHover: { x: 4 }, whileTap: { scale: 0.98 }, children: [jsxRuntime.jsx("span", { className: styles$7.icon, children: t.icon }), jsxRuntime.jsx("span", { className: styles$7.text, children: t.label })] }, t.value))) })] }));
1535
+ return (jsxRuntime.jsxs("div", { className: `${styles$7.dropdown} ${className}`, children: [jsxRuntime.jsxs(framerMotion.motion.button, { className: styles$7.dropdownTrigger, whileTap: { scale: 0.98 }, children: [currentThemeData.icon, showLabel && jsxRuntime.jsx("span", { className: styles$7.label, children: currentThemeData.label })] }), jsxRuntime.jsx(framerMotion.motion.div, { className: styles$7.dropdownMenu, initial: { opacity: 0, y: -10 }, animate: { opacity: 1, y: 0 }, children: themes.map((t) => (jsxRuntime.jsxs(framerMotion.motion.button, { className: `${styles$7.dropdownItem} ${theme === t.value ? styles$7.active : ''}`, onClick: () => setTheme(t.value), whileHover: { x: 4 }, whileTap: { scale: 0.98 }, children: [jsxRuntime.jsx("span", { className: styles$7.icon, children: t.icon }), jsxRuntime.jsx("span", { className: styles$7.text, children: t.label })] }, t.value))) })] }));
1522
1536
  }
1523
1537
  // Default button variant - cycles through themes
1524
1538
  return (jsxRuntime.jsxs(framerMotion.motion.button, { className: `${styles$7.button} ${className}`, onClick: () => {
1525
1539
  const nextIndex = (currentThemeIndex + 1) % themes.length;
1526
1540
  setTheme(themes[nextIndex].value);
1527
- }, whileTap: { scale: 0.95 }, whileHover: { scale: 1.05 }, "aria-label": `Current theme: ${currentTheme.label}. Click to change.`, children: [jsxRuntime.jsx(framerMotion.motion.div, { initial: { rotate: -180, opacity: 0 }, animate: { rotate: 0, opacity: 1 }, exit: { rotate: 180, opacity: 0 }, transition: { duration: 0.3 }, className: styles$7.iconWrapper, children: currentTheme.icon }, theme), showLabel && jsxRuntime.jsx("span", { className: styles$7.label, children: currentTheme.label })] }));
1541
+ }, whileTap: { scale: 0.95 }, whileHover: { scale: 1.05 }, "aria-label": `Current theme: ${currentThemeData.label}. Click to change.`, children: [jsxRuntime.jsx(framerMotion.motion.div, { initial: { rotate: -180, opacity: 0 }, animate: { rotate: 0, opacity: 1 }, exit: { rotate: 180, opacity: 0 }, transition: { duration: 0.3 }, className: styles$7.iconWrapper, children: currentThemeData.icon }, theme), showLabel && jsxRuntime.jsx("span", { className: styles$7.label, children: currentThemeData.label })] }));
1528
1542
  };
1529
1543
 
1530
1544
  // THIS FILE IS AUTO GENERATED
@@ -1534,15 +1548,16 @@ function SiJira (props) {
1534
1548
 
1535
1549
  var styles$6 = {"tabs":"Tabs-module_tabs__Vlvn7","tab":"Tabs-module_tab__uQKim","tabIcon":"Tabs-module_tabIcon__AgN-O"};
1536
1550
 
1537
- const tabs = [
1551
+ // Default tabs for backwards compatibility
1552
+ const defaultTabs = [
1538
1553
  { id: 'details', icon: FiInfo, label: 'Dettagli' },
1539
1554
  { id: 'github', icon: FiGithub, label: 'GitHub' },
1540
1555
  { id: 'jira', icon: SiJira, label: 'Jira' },
1541
1556
  { id: 'functional', icon: FiInfo, label: 'Analisi funzionale' }
1542
1557
  ];
1543
- const Tabs = ({ activeTab, onTabChange }) => {
1544
- return (jsxRuntime.jsx("div", { className: styles$6.tabs, children: tabs.map((tab) => {
1545
- const Icon = tab.icon;
1558
+ const Tabs = ({ activeTab, onTabChange, tabs: customTabs, className = '' }) => {
1559
+ const tabs = customTabs ?? defaultTabs;
1560
+ return (jsxRuntime.jsx("div", { className: `${styles$6.tabs} ${className}`, children: tabs.map((tab) => {
1546
1561
  const isActive = activeTab === tab.id;
1547
1562
  return (jsxRuntime.jsxs(framerMotion.motion.button, { className: styles$6.tab, "data-active": isActive, onClick: () => onTabChange(tab.id), style: { position: 'relative' }, children: [jsxRuntime.jsx(framerMotion.motion.div, { animate: {
1548
1563
  rotate: isActive ? [0, -10, 10, -5, 5, 0] : 0,
@@ -1551,7 +1566,7 @@ const Tabs = ({ activeTab, onTabChange }) => {
1551
1566
  duration: 0.5,
1552
1567
  ease: 'easeInOut'
1553
1568
  }
1554
- }, children: jsxRuntime.jsx(Icon, { className: styles$6.tabIcon }) }), jsxRuntime.jsx("span", { children: tab.label })] }, tab.id));
1569
+ }, children: tab.icon && (typeof tab.icon === 'function' ? (jsxRuntime.jsx("span", { className: styles$6.tabIcon, children: React.createElement(tab.icon) })) : (jsxRuntime.jsx("span", { className: styles$6.tabIcon, children: tab.icon }))) }), jsxRuntime.jsx("span", { children: tab.label })] }, tab.id));
1555
1570
  }) }));
1556
1571
  };
1557
1572
 
@@ -2235,11 +2250,22 @@ const BooleansHeatmap = ({ data, habitName, width = 800, height = 200, habitColo
2235
2250
  var styles$1 = {"container":"SunburstChart-module_container__w1ZYc","title":"SunburstChart-module_title__T6Ak7","chart":"SunburstChart-module_chart__BFM6E","tooltip":"SunburstChart-module_tooltip__TuTAN"};
2236
2251
 
2237
2252
  const COLOR_PALETTE = [
2238
- '#d4af37', '#FFD700', '#FFA500', '#FF8C00',
2239
- '#FF6347', '#DC143C', '#8B4513', '#A0522D',
2240
- '#DEB887', '#F4A460', '#D2691E', '#CD853F'
2253
+ '#6366f1', '#8b5cf6', '#06b6d4', '#10b981',
2254
+ '#f59e0b', '#ef4444', '#ec4899', '#84cc16',
2255
+ '#f97316', '#3b82f6', '#14b8a6', '#f59e0b'
2241
2256
  ];
2242
- const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Chart', tagColors = {} }) => {
2257
+ // Calculate text color based on background luminance for optimal contrast
2258
+ const getTextColor$1 = (backgroundColor) => {
2259
+ const color = d3__namespace.color(backgroundColor);
2260
+ if (!color)
2261
+ return '#ffffff';
2262
+ const rgb = color.rgb();
2263
+ // Calculate relative luminance using WCAG formula
2264
+ const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
2265
+ // Return white text for dark backgrounds, black for light backgrounds
2266
+ return luminance > 0.5 ? '#000000' : '#ffffff';
2267
+ };
2268
+ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Chart', tagColors = {}, unit = 'items', centerLabel }) => {
2243
2269
  const svgRef = React.useRef(null);
2244
2270
  const colorMap = React.useRef(new Map()).current;
2245
2271
  const colorIndex = React.useRef(0);
@@ -2282,7 +2308,8 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
2282
2308
  .startAngle(d => d.x0)
2283
2309
  .endAngle(d => d.x1)
2284
2310
  .innerRadius(d => Math.sqrt(d.y0))
2285
- .outerRadius(d => Math.sqrt(d.y1));
2311
+ .outerRadius(d => Math.sqrt(d.y1))
2312
+ .cornerRadius(3);
2286
2313
  const tooltip = d3__namespace.select('body').append('div')
2287
2314
  .attr('class', styles$1.tooltip)
2288
2315
  .style('opacity', 0)
@@ -2301,11 +2328,20 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
2301
2328
  .attr('stroke', 'var(--bg-primary)')
2302
2329
  .attr('stroke-width', 2)
2303
2330
  .style('cursor', 'pointer')
2331
+ .style('filter', 'drop-shadow(0 1px 3px rgba(0,0,0,0.12))')
2304
2332
  .on('mouseover', function (event, d) {
2333
+ const hoverArc = d3__namespace.arc()
2334
+ .startAngle(d => d.x0)
2335
+ .endAngle(d => d.x1)
2336
+ .innerRadius(d => Math.sqrt(d.y0) - 2)
2337
+ .outerRadius(d => Math.sqrt(d.y1) + 4)
2338
+ .cornerRadius(3);
2305
2339
  d3__namespace.select(this)
2306
2340
  .transition()
2307
- .duration(200)
2308
- .style('opacity', 0.8);
2341
+ .duration(150)
2342
+ .attr('d', d => hoverArc(d))
2343
+ .style('filter', 'drop-shadow(0 2px 8px rgba(0,0,0,0.2))')
2344
+ .style('opacity', 0.9);
2309
2345
  tooltip.transition()
2310
2346
  .duration(200)
2311
2347
  .style('opacity', 1);
@@ -2319,10 +2355,12 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
2319
2355
  .style('left', (event.pageX + 10) + 'px')
2320
2356
  .style('top', (event.pageY - 28) + 'px');
2321
2357
  })
2322
- .on('mouseout', function () {
2358
+ .on('mouseout', function (event, d) {
2323
2359
  d3__namespace.select(this)
2324
2360
  .transition()
2325
- .duration(200)
2361
+ .duration(150)
2362
+ .attr('d', d => arc(d))
2363
+ .style('filter', 'drop-shadow(0 1px 3px rgba(0,0,0,0.12))')
2326
2364
  .style('opacity', 1);
2327
2365
  tooltip.transition()
2328
2366
  .duration(500)
@@ -2332,9 +2370,32 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
2332
2370
  const angle = d.x1 - d.x0;
2333
2371
  return angle > 0.15 && d.depth <= 2;
2334
2372
  };
2335
- g.selectAll('text')
2373
+ // Calculate average background color for center text contrast
2374
+ const allSegments = nodes.filter(d => d.depth === 1);
2375
+ const avgColor = allSegments.length > 0 ? d3__namespace.interpolateRgb.gamma(2.2)(...allSegments.map(d => getColor(d.data.name, 1)))(0.5) : '#ffffff';
2376
+ const centerTextColor = getTextColor$1(avgColor);
2377
+ // Add center text
2378
+ g.append('text')
2379
+ .attr('text-anchor', 'middle')
2380
+ .attr('alignment-baseline', 'middle')
2381
+ .attr('font-size', '18px')
2382
+ .attr('font-weight', 'bold')
2383
+ .attr('fill', centerTextColor)
2384
+ .style('text-shadow', centerTextColor === '#ffffff' ? '0 1px 3px rgba(0,0,0,0.5)' : '0 1px 3px rgba(255,255,255,0.5)')
2385
+ .text(centerLabel || data.name || 'Total');
2386
+ g.append('text')
2387
+ .attr('text-anchor', 'middle')
2388
+ .attr('alignment-baseline', 'middle')
2389
+ .attr('y', 20)
2390
+ .attr('font-size', '14px')
2391
+ .attr('font-weight', '500')
2392
+ .attr('fill', centerTextColor)
2393
+ .style('text-shadow', centerTextColor === '#ffffff' ? '0 1px 2px rgba(0,0,0,0.4)' : '0 1px 2px rgba(255,255,255,0.4)')
2394
+ .text(`${(root.value || 0).toLocaleString()} ${unit}`);
2395
+ g.selectAll('text.segment-label')
2336
2396
  .data(nodes.filter(d => d.depth > 0 && d.value && d.value > 0 && shouldDisplayLabel(d)))
2337
2397
  .enter().append('text')
2398
+ .attr('class', 'segment-label')
2338
2399
  .attr('transform', d => {
2339
2400
  const angle = (d.x0 + d.x1) / 2;
2340
2401
  const radius = (Math.sqrt(d.y0) + Math.sqrt(d.y1)) / 2;
@@ -2344,11 +2405,32 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
2344
2405
  })
2345
2406
  .attr('text-anchor', 'middle')
2346
2407
  .attr('alignment-baseline', 'middle')
2347
- .attr('font-size', '12px')
2348
- .attr('fill', 'var(--text-inverse)')
2349
- .attr('font-weight', 'var(--font-medium)')
2408
+ .attr('font-size', d => d.depth === 1 ? '13px' : '11px')
2409
+ .attr('fill', d => {
2410
+ let ancestor = d;
2411
+ while (ancestor.depth > 1 && ancestor.parent) {
2412
+ ancestor = ancestor.parent;
2413
+ }
2414
+ const segmentColor = getColor(ancestor.data.name, d.depth);
2415
+ return getTextColor$1(segmentColor);
2416
+ })
2417
+ .attr('font-weight', '600')
2350
2418
  .style('pointer-events', 'none')
2351
- .text(d => d.data.name.substring(0, 10));
2419
+ .style('text-shadow', d => {
2420
+ let ancestor = d;
2421
+ while (ancestor.depth > 1 && ancestor.parent) {
2422
+ ancestor = ancestor.parent;
2423
+ }
2424
+ const segmentColor = getColor(ancestor.data.name, d.depth);
2425
+ const textColor = getTextColor$1(segmentColor);
2426
+ // Use contrasting shadow color
2427
+ const shadowColor = textColor === '#ffffff' ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.5)';
2428
+ return `0 1px 2px ${shadowColor}`;
2429
+ })
2430
+ .text(d => {
2431
+ const maxLength = d.depth === 1 ? 12 : 8;
2432
+ return d.data.name.substring(0, maxLength);
2433
+ });
2352
2434
  return () => {
2353
2435
  tooltip.remove();
2354
2436
  };
@@ -2359,12 +2441,23 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
2359
2441
  var styles = {"container":"PieChart-module_container__tXjbe","title":"PieChart-module_title__61o0R","chartContainer":"PieChart-module_chartContainer__uLmOz","chart":"PieChart-module_chart__3nqON","legend":"PieChart-module_legend__rAWgh","legendItem":"PieChart-module_legendItem__Nb031","legendColor":"PieChart-module_legendColor__fLuv9","legendLabel":"PieChart-module_legendLabel__xbjBr","legendValue":"PieChart-module_legendValue__h2WS2","tooltip":"PieChart-module_tooltip__140RU"};
2360
2442
 
2361
2443
  const DEFAULT_COLORS = [
2362
- '#d4af37', '#FFD700', '#FFA500', '#FF8C00',
2363
- '#FF6347', '#DC143C', '#8B4513', '#A0522D',
2364
- '#DEB887', '#F4A460', '#D2691E', '#CD853F',
2365
- '#B8860B', '#DAA520', '#F0E68C', '#BDB76B'
2444
+ '#6366f1', '#8b5cf6', '#06b6d4', '#10b981',
2445
+ '#f59e0b', '#ef4444', '#ec4899', '#84cc16',
2446
+ '#f97316', '#3b82f6', '#8b5cf6', '#14b8a6',
2447
+ '#f59e0b', '#ef4444', '#06b6d4', '#10b981'
2366
2448
  ];
2367
- const PieChart = ({ data, width = 400, height = 400, title = 'Distribution', showLegend = true }) => {
2449
+ // Calculate text color based on background luminance for optimal contrast
2450
+ const getTextColor = (backgroundColor) => {
2451
+ const color = d3__namespace.color(backgroundColor);
2452
+ if (!color)
2453
+ return '#ffffff';
2454
+ const rgb = color.rgb();
2455
+ // Calculate relative luminance using WCAG formula
2456
+ const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
2457
+ // Return white text for dark backgrounds, black for light backgrounds
2458
+ return luminance > 0.5 ? '#000000' : '#ffffff';
2459
+ };
2460
+ const PieChart = ({ data, width = 400, height = 400, title = 'Distribution', showLegend = true, unit = 'items', centerLabel }) => {
2368
2461
  const svgRef = React.useRef(null);
2369
2462
  const radius = Math.min(width, height) / 2 - 20;
2370
2463
  React.useEffect(() => {
@@ -2378,8 +2471,9 @@ const PieChart = ({ data, width = 400, height = 400, title = 'Distribution', sho
2378
2471
  .value(d => d.value)
2379
2472
  .sort(null);
2380
2473
  const arc = d3__namespace.arc()
2381
- .innerRadius(0)
2382
- .outerRadius(radius);
2474
+ .innerRadius(radius * 0.4)
2475
+ .outerRadius(radius)
2476
+ .cornerRadius(4);
2383
2477
  const labelArc = d3__namespace.arc()
2384
2478
  .innerRadius(radius * 0.7)
2385
2479
  .outerRadius(radius * 0.7);
@@ -2397,13 +2491,19 @@ const PieChart = ({ data, width = 400, height = 400, title = 'Distribution', sho
2397
2491
  .attr('d', d => arc(d))
2398
2492
  .attr('fill', (d, i) => d.data.color || DEFAULT_COLORS[i % DEFAULT_COLORS.length])
2399
2493
  .attr('stroke', 'var(--bg-primary)')
2400
- .attr('stroke-width', 2)
2494
+ .attr('stroke-width', 3)
2401
2495
  .style('cursor', 'pointer')
2496
+ .style('filter', 'drop-shadow(0 2px 8px rgba(0,0,0,0.15))')
2402
2497
  .on('mouseover', function (event, d) {
2498
+ const hoverArc = d3__namespace.arc()
2499
+ .innerRadius(radius * 0.4)
2500
+ .outerRadius(radius * 1.05)
2501
+ .cornerRadius(4);
2403
2502
  d3__namespace.select(this)
2404
2503
  .transition()
2405
2504
  .duration(200)
2406
- .style('opacity', 0.8);
2505
+ .attr('d', d => hoverArc(d))
2506
+ .style('filter', 'drop-shadow(0 4px 12px rgba(0,0,0,0.25))');
2407
2507
  tooltip.transition()
2408
2508
  .duration(200)
2409
2509
  .style('opacity', 1);
@@ -2416,27 +2516,59 @@ const PieChart = ({ data, width = 400, height = 400, title = 'Distribution', sho
2416
2516
  .style('left', (event.pageX + 10) + 'px')
2417
2517
  .style('top', (event.pageY - 28) + 'px');
2418
2518
  })
2419
- .on('mouseout', function () {
2519
+ .on('mouseout', function (event, d) {
2420
2520
  d3__namespace.select(this)
2421
2521
  .transition()
2422
2522
  .duration(200)
2423
- .style('opacity', 1);
2523
+ .attr('d', d => arc(d))
2524
+ .style('filter', 'drop-shadow(0 2px 8px rgba(0,0,0,0.15))');
2424
2525
  tooltip.transition()
2425
2526
  .duration(500)
2426
2527
  .style('opacity', 0);
2427
2528
  });
2428
- arcs.filter(d => (d.endAngle - d.startAngle) > 0.3)
2529
+ // Calculate average background color for center text contrast
2530
+ const avgColor = data.length > 0 ? d3__namespace.interpolateRgb.gamma(2.2)(...data.map((d, i) => d.color || DEFAULT_COLORS[i % DEFAULT_COLORS.length]))(0.5) : '#ffffff';
2531
+ const centerTextColor = getTextColor(avgColor);
2532
+ // Add center text for donut
2533
+ g.append('text')
2534
+ .attr('text-anchor', 'middle')
2535
+ .attr('alignment-baseline', 'middle')
2536
+ .attr('font-size', '20px')
2537
+ .attr('font-weight', 'bold')
2538
+ .attr('fill', centerTextColor)
2539
+ .style('text-shadow', centerTextColor === '#ffffff' ? '0 1px 3px rgba(0,0,0,0.5)' : '0 1px 3px rgba(255,255,255,0.5)')
2540
+ .text(centerLabel || 'Total');
2541
+ g.append('text')
2542
+ .attr('text-anchor', 'middle')
2543
+ .attr('alignment-baseline', 'middle')
2544
+ .attr('y', 22)
2545
+ .attr('font-size', '16px')
2546
+ .attr('font-weight', '600')
2547
+ .attr('fill', centerTextColor)
2548
+ .style('text-shadow', centerTextColor === '#ffffff' ? '0 1px 2px rgba(0,0,0,0.4)' : '0 1px 2px rgba(255,255,255,0.4)')
2549
+ .text(`${total.toLocaleString()} ${unit}`);
2550
+ arcs.filter(d => (d.endAngle - d.startAngle) > 0.2)
2429
2551
  .append('text')
2430
2552
  .attr('transform', d => `translate(${labelArc.centroid(d)})`)
2431
2553
  .attr('text-anchor', 'middle')
2432
2554
  .attr('alignment-baseline', 'middle')
2433
- .attr('font-size', '12px')
2434
- .attr('fill', 'var(--text-inverse)')
2435
- .attr('font-weight', 'var(--font-semibold)')
2555
+ .attr('font-size', '13px')
2556
+ .attr('fill', (d, i) => {
2557
+ const segmentColor = d.data.color || DEFAULT_COLORS[i % DEFAULT_COLORS.length];
2558
+ return getTextColor(segmentColor);
2559
+ })
2560
+ .attr('font-weight', '600')
2436
2561
  .style('pointer-events', 'none')
2562
+ .style('text-shadow', (d, i) => {
2563
+ const segmentColor = d.data.color || DEFAULT_COLORS[i % DEFAULT_COLORS.length];
2564
+ const textColor = getTextColor(segmentColor);
2565
+ // Use contrasting shadow color
2566
+ const shadowColor = textColor === '#ffffff' ? 'rgba(0,0,0,0.4)' : 'rgba(255,255,255,0.4)';
2567
+ return `0 1px 2px ${shadowColor}`;
2568
+ })
2437
2569
  .text(d => {
2438
2570
  const percentage = ((d.data.value / total) * 100);
2439
- return percentage > 5 ? `${percentage.toFixed(1)}%` : '';
2571
+ return percentage > 8 ? `${percentage.toFixed(0)}%` : '';
2440
2572
  });
2441
2573
  return () => {
2442
2574
  tooltip.remove();