@shellui/core 0.2.0-alpha.4 → 0.2.0-beta.1

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.
Files changed (61) hide show
  1. package/package.json +2 -2
  2. package/src/app.tsx +2 -2
  3. package/src/components/ContentView.tsx +70 -135
  4. package/src/components/LoadingOverlay.tsx +5 -1
  5. package/src/components/ui/sidebar.tsx +2 -124
  6. package/src/constants/loading.ts +2 -0
  7. package/src/features/config/types.ts +2 -0
  8. package/src/features/layouts/AppLayout.tsx +22 -19
  9. package/src/features/layouts/LayoutFallback.tsx +8 -0
  10. package/src/features/layouts/OverlayShell.tsx +23 -9
  11. package/src/features/layouts/{AppBarLayout.tsx → appbar/AppBarLayout.tsx} +72 -78
  12. package/src/features/layouts/{FullscreenLayout.tsx → fullscreen/FullscreenLayout.tsx} +5 -11
  13. package/src/features/layouts/sidebar/BottomNavItem.tsx +88 -0
  14. package/src/features/layouts/sidebar/MobileBottomNav.tsx +168 -0
  15. package/src/features/layouts/sidebar/NavigationContent.tsx +159 -0
  16. package/src/features/layouts/sidebar/SidebarIcons.tsx +93 -0
  17. package/src/features/layouts/sidebar/SidebarInner.tsx +48 -0
  18. package/src/features/layouts/sidebar/SidebarLayout.tsx +86 -0
  19. package/src/features/layouts/sidebar/sidebarUtils.ts +23 -0
  20. package/src/features/layouts/sidebar/types.ts +8 -0
  21. package/src/features/layouts/utils.ts +29 -1
  22. package/src/features/layouts/{WindowsLayout.tsx → windows/WindowsLayout.tsx} +199 -204
  23. package/src/features/settings/SettingsView.tsx +178 -181
  24. package/src/{components → routes/components}/HomeView.tsx +1 -1
  25. package/src/{components → routes/components}/IndexRoute.tsx +4 -4
  26. package/src/routes/components/NavigationItemRoute.tsx +19 -0
  27. package/src/{components → routes/components}/NotFoundView.tsx +9 -4
  28. package/src/{components → routes/components}/RouteErrorBoundary.tsx +1 -1
  29. package/src/routes/components/RouteFallback.tsx +8 -0
  30. package/src/routes/hooks/useNavigationItems.ts +84 -0
  31. package/src/{router → routes}/routes.tsx +18 -16
  32. package/src/components/ViewRoute.tsx +0 -48
  33. package/src/dist/CookiePreferencesView.52b5aec8.js +0 -1182
  34. package/src/dist/CookiePreferencesView.52b5aec8.js.map +0 -1
  35. package/src/dist/DefaultLayout.045a82ff.js +0 -1964
  36. package/src/dist/DefaultLayout.045a82ff.js.map +0 -1
  37. package/src/dist/DefaultLayout.4454f259.js +0 -4414
  38. package/src/dist/DefaultLayout.4454f259.js.map +0 -1
  39. package/src/dist/FullscreenLayout.555c4987.js +0 -1054
  40. package/src/dist/FullscreenLayout.555c4987.js.map +0 -1
  41. package/src/dist/HomeView.ddfa7b68.js +0 -771
  42. package/src/dist/HomeView.ddfa7b68.js.map +0 -1
  43. package/src/dist/NotFoundView.c75be4f1.js +0 -811
  44. package/src/dist/NotFoundView.c75be4f1.js.map +0 -1
  45. package/src/dist/SettingsView.052b03a6.js +0 -4965
  46. package/src/dist/SettingsView.052b03a6.js.map +0 -1
  47. package/src/dist/ViewRoute.e6e3b142.js +0 -1042
  48. package/src/dist/ViewRoute.e6e3b142.js.map +0 -1
  49. package/src/dist/WindowsLayout.08724167.js +0 -1762
  50. package/src/dist/WindowsLayout.08724167.js.map +0 -1
  51. package/src/dist/esm.f0d741e6.js +0 -29520
  52. package/src/dist/esm.f0d741e6.js.map +0 -1
  53. package/src/dist/favicon.4367ac1e.svg +0 -14
  54. package/src/dist/index.parcel.36d65383.js +0 -54089
  55. package/src/dist/index.parcel.36d65383.js.map +0 -1
  56. package/src/dist/index.parcel.ca6d8a47.css +0 -3493
  57. package/src/dist/index.parcel.ca6d8a47.css.map +0 -1
  58. package/src/dist/index.parcel.html +0 -88
  59. package/src/features/layouts/DefaultLayout.tsx +0 -660
  60. package/src/features/layouts/LayoutProviders.tsx +0 -20
  61. /package/src/{router → routes}/router.tsx +0 -0
@@ -9,20 +9,18 @@ import {
9
9
  import { useLocation } from 'react-router';
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import { shellui } from '@shellui/sdk';
12
- import type { NavigationItem, NavigationGroup } from '../config/types';
12
+ import type { NavigationItem, NavigationGroup } from '../../config/types';
13
13
  import {
14
14
  flattenNavigationItems,
15
15
  getActivePathPrefix,
16
16
  getNavPathPrefix,
17
17
  resolveLocalizedString as resolveNavLabel,
18
18
  splitNavigationByPosition,
19
- } from './utils';
20
- import { useSettings } from '../settings/hooks/useSettings';
21
- import { LayoutProviders } from './LayoutProviders';
22
- import { OverlayShell } from './OverlayShell';
23
- import { ContentView } from '../../components/ContentView';
24
- import { cn } from '../../lib/utils';
25
- import { Z_INDEX } from '../../lib/z-index';
19
+ } from '../utils';
20
+ import { useSettings } from '../../settings/SettingsContext';
21
+ import { ContentView } from '../../../components/ContentView';
22
+ import { cn } from '../../../lib/utils';
23
+ import { Z_INDEX } from '../../../lib/z-index';
26
24
 
27
25
  interface WindowsLayoutProps {
28
26
  title?: string;
@@ -650,233 +648,230 @@ export function WindowsLayout({
650
648
  );
651
649
 
652
650
  return (
653
- <LayoutProviders>
654
- <OverlayShell navigationItems={navigationItems}>
651
+ <>
652
+ <div
653
+ className="fixed inset-0 bg-muted/30"
654
+ style={{ paddingBottom: TASKBAR_HEIGHT }}
655
+ >
656
+ {/* Desktop area: windows */}
657
+ {windows.map((win, index) => {
658
+ const navItem = navigationItems.find((n) => n.path === win.path);
659
+ if (!navItem) return null;
660
+ const isFocused = win.id === frontWindowId;
661
+ const zIndex = Z_INDEX.WINDOWS_WINDOW_BASE + index;
662
+ return (
663
+ <AppWindow
664
+ key={win.id}
665
+ win={win}
666
+ navItem={navItem}
667
+ currentLanguage={currentLanguage}
668
+ isFocused={isFocused}
669
+ onFocus={() => focusWindow(win.id)}
670
+ onClose={() => closeWindow(win.id)}
671
+ onBoundsChange={(bounds) => updateWindowBounds(win.id, bounds)}
672
+ maxZIndex={maxZIndex}
673
+ zIndex={zIndex}
674
+ />
675
+ );
676
+ })}
677
+ </div>
678
+
679
+ {/* Taskbar */}
680
+ <div
681
+ className="fixed left-0 right-0 bottom-0 flex items-center gap-1 px-2 border-t border-border bg-sidebar-background"
682
+ style={{
683
+ height: TASKBAR_HEIGHT,
684
+ zIndex: Z_INDEX.WINDOWS_TASKBAR,
685
+ paddingBottom: 'env(safe-area-inset-bottom, 0px)',
686
+ }}
687
+ >
688
+ {/* Start button */}
655
689
  <div
656
- className="fixed inset-0 bg-muted/30"
657
- style={{ paddingBottom: TASKBAR_HEIGHT }}
690
+ className="relative shrink-0"
691
+ ref={startPanelRef}
658
692
  >
659
- {/* Desktop area: windows */}
660
- {windows.map((win, index) => {
693
+ <button
694
+ type="button"
695
+ onClick={() => setStartMenuOpen((o) => !o)}
696
+ className={cn(
697
+ 'flex items-center gap-2 h-9 px-3 rounded cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
698
+ startMenuOpen
699
+ ? 'bg-sidebar-accent text-sidebar-accent-foreground'
700
+ : 'text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
701
+ )}
702
+ aria-expanded={startMenuOpen}
703
+ aria-haspopup="true"
704
+ aria-label="Start"
705
+ >
706
+ <StartIcon className="h-5 w-5" />
707
+ <span className="font-semibold text-sm hidden sm:inline">{title || 'Start'}</span>
708
+ </button>
709
+ {/* Start menu panel */}
710
+ {startMenuOpen && (
711
+ <div
712
+ className="absolute bottom-full left-0 mb-1 w-64 max-h-[70vh] overflow-y-auto rounded-lg border border-border bg-popover shadow-lg py-2 z-[10001]"
713
+ style={{ zIndex: Z_INDEX.MODAL_CONTENT }}
714
+ >
715
+ <div className="px-2 pb-2 border-b border-border mb-2">
716
+ <span className="text-sm font-semibold text-popover-foreground">
717
+ {title || 'Applications'}
718
+ </span>
719
+ </div>
720
+ <div className="grid gap-0.5">
721
+ {startNavItems
722
+ .filter((item) => !item.hidden)
723
+ .map((item) => {
724
+ const label =
725
+ typeof item.label === 'string'
726
+ ? item.label
727
+ : resolveNavLabel(item.label, currentLanguage);
728
+ const icon =
729
+ item.icon ??
730
+ (item.openIn === 'external' ? getExternalFaviconUrl(item.url) : null);
731
+ return (
732
+ <button
733
+ key={item.path}
734
+ type="button"
735
+ onClick={() => handleNavClick(item)}
736
+ className="flex items-center gap-3 w-full px-3 py-2 text-left text-sm cursor-pointer text-popover-foreground hover:bg-accent hover:text-accent-foreground rounded-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
737
+ >
738
+ {icon ? (
739
+ <img
740
+ src={icon}
741
+ alt=""
742
+ className={cn(
743
+ 'h-5 w-5 shrink-0 rounded-sm object-cover',
744
+ isAppIcon(icon) && 'opacity-90 dark:opacity-100 dark:invert',
745
+ )}
746
+ />
747
+ ) : (
748
+ <span className="h-5 w-5 shrink-0 rounded-sm bg-muted" />
749
+ )}
750
+ <span className="truncate">{label}</span>
751
+ </button>
752
+ );
753
+ })}
754
+ </div>
755
+ </div>
756
+ )}
757
+ </div>
758
+
759
+ {/* Window list */}
760
+ <div className="flex-1 flex items-center gap-1 min-w-0 overflow-x-auto">
761
+ {windows.map((win) => {
661
762
  const navItem = navigationItems.find((n) => n.path === win.path);
662
- if (!navItem) return null;
763
+ const windowLabel = navItem
764
+ ? resolveNavLabel(navItem.label, currentLanguage)
765
+ : win.label;
663
766
  const isFocused = win.id === frontWindowId;
664
- const zIndex = Z_INDEX.WINDOWS_WINDOW_BASE + index;
665
767
  return (
666
- <AppWindow
768
+ <button
667
769
  key={win.id}
668
- win={win}
669
- navItem={navItem}
670
- currentLanguage={currentLanguage}
671
- isFocused={isFocused}
672
- onFocus={() => focusWindow(win.id)}
673
- onClose={() => closeWindow(win.id)}
674
- onBoundsChange={(bounds) => updateWindowBounds(win.id, bounds)}
675
- maxZIndex={maxZIndex}
676
- zIndex={zIndex}
677
- />
770
+ type="button"
771
+ onClick={() => focusWindow(win.id)}
772
+ onContextMenu={(e) => {
773
+ e.preventDefault();
774
+ closeWindow(win.id);
775
+ }}
776
+ className={cn(
777
+ 'flex items-center gap-2 h-8 px-2 rounded min-w-0 max-w-[140px] shrink-0 cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
778
+ isFocused
779
+ ? 'bg-sidebar-accent text-sidebar-accent-foreground'
780
+ : 'text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
781
+ )}
782
+ title={windowLabel}
783
+ >
784
+ {win.icon ? (
785
+ <img
786
+ src={win.icon}
787
+ alt=""
788
+ className={cn(
789
+ 'h-4 w-4 shrink-0 rounded-sm object-cover',
790
+ isAppIcon(win.icon) && 'opacity-90 dark:opacity-100 dark:invert',
791
+ )}
792
+ />
793
+ ) : (
794
+ <span className="h-4 w-4 shrink-0 rounded-sm bg-muted" />
795
+ )}
796
+ <span className="text-xs truncate">{windowLabel}</span>
797
+ </button>
678
798
  );
679
799
  })}
680
800
  </div>
681
801
 
682
- {/* Taskbar */}
683
- <div
684
- className="fixed left-0 right-0 bottom-0 flex items-center gap-1 px-2 border-t border-border bg-sidebar-background"
685
- style={{
686
- height: TASKBAR_HEIGHT,
687
- zIndex: Z_INDEX.WINDOWS_TASKBAR,
688
- paddingBottom: 'env(safe-area-inset-bottom, 0px)',
689
- }}
690
- >
691
- {/* Start button */}
692
- <div
693
- className="relative shrink-0"
694
- ref={startPanelRef}
695
- >
696
- <button
697
- type="button"
698
- onClick={() => setStartMenuOpen((o) => !o)}
699
- className={cn(
700
- 'flex items-center gap-2 h-9 px-3 rounded cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
701
- startMenuOpen
702
- ? 'bg-sidebar-accent text-sidebar-accent-foreground'
703
- : 'text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
704
- )}
705
- aria-expanded={startMenuOpen}
706
- aria-haspopup="true"
707
- aria-label="Start"
708
- >
709
- <StartIcon className="h-5 w-5" />
710
- <span className="font-semibold text-sm hidden sm:inline">{title || 'Start'}</span>
711
- </button>
712
- {/* Start menu panel */}
713
- {startMenuOpen && (
714
- <div
715
- className="absolute bottom-full left-0 mb-1 w-64 max-h-[70vh] overflow-y-auto rounded-lg border border-border bg-popover shadow-lg py-2 z-[10001]"
716
- style={{ zIndex: Z_INDEX.MODAL_CONTENT }}
717
- >
718
- <div className="px-2 pb-2 border-b border-border mb-2">
719
- <span className="text-sm font-semibold text-popover-foreground">
720
- {title || 'Applications'}
721
- </span>
722
- </div>
723
- <div className="grid gap-0.5">
724
- {startNavItems
725
- .filter((item) => !item.hidden)
726
- .map((item) => {
727
- const label =
728
- typeof item.label === 'string'
729
- ? item.label
730
- : resolveNavLabel(item.label, currentLanguage);
731
- const icon =
732
- item.icon ??
733
- (item.openIn === 'external' ? getExternalFaviconUrl(item.url) : null);
734
- return (
735
- <button
736
- key={item.path}
737
- type="button"
738
- onClick={() => handleNavClick(item)}
739
- className="flex items-center gap-3 w-full px-3 py-2 text-left text-sm cursor-pointer text-popover-foreground hover:bg-accent hover:text-accent-foreground rounded-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
740
- >
741
- {icon ? (
742
- <img
743
- src={icon}
744
- alt=""
745
- className={cn(
746
- 'h-5 w-5 shrink-0 rounded-sm object-cover',
747
- isAppIcon(icon) && 'opacity-90 dark:opacity-100 dark:invert',
748
- )}
749
- />
750
- ) : (
751
- <span className="h-5 w-5 shrink-0 rounded-sm bg-muted" />
752
- )}
753
- <span className="truncate">{label}</span>
754
- </button>
755
- );
756
- })}
757
- </div>
758
- </div>
759
- )}
760
- </div>
761
-
762
- {/* Window list */}
763
- <div className="flex-1 flex items-center gap-1 min-w-0 overflow-x-auto">
764
- {windows.map((win) => {
765
- const navItem = navigationItems.find((n) => n.path === win.path);
766
- const windowLabel = navItem
767
- ? resolveNavLabel(navItem.label, currentLanguage)
768
- : win.label;
769
- const isFocused = win.id === frontWindowId;
802
+ {/* End navigation items (right side of taskbar) */}
803
+ {endNavItems.length > 0 && (
804
+ <div className="flex items-center gap-0.5 shrink-0 border-l border-sidebar-border pl-2 ml-1">
805
+ {endNavItems.map((item) => {
806
+ const label =
807
+ typeof item.label === 'string'
808
+ ? item.label
809
+ : resolveNavLabel(item.label, currentLanguage);
810
+ const icon =
811
+ item.icon ?? (item.openIn === 'external' ? getExternalFaviconUrl(item.url) : null);
770
812
  return (
771
813
  <button
772
- key={win.id}
814
+ key={item.path}
773
815
  type="button"
774
- onClick={() => focusWindow(win.id)}
775
- onContextMenu={(e) => {
776
- e.preventDefault();
777
- closeWindow(win.id);
778
- }}
779
- className={cn(
780
- 'flex items-center gap-2 h-8 px-2 rounded min-w-0 max-w-[140px] shrink-0 cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
781
- isFocused
782
- ? 'bg-sidebar-accent text-sidebar-accent-foreground'
783
- : 'text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
784
- )}
785
- title={windowLabel}
816
+ onClick={() => handleNavClick(item)}
817
+ className="flex items-center gap-2 h-8 px-2 rounded cursor-pointer text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
818
+ title={label}
786
819
  >
787
- {win.icon ? (
820
+ {icon ? (
788
821
  <img
789
- src={win.icon}
822
+ src={icon}
790
823
  alt=""
791
824
  className={cn(
792
825
  'h-4 w-4 shrink-0 rounded-sm object-cover',
793
- isAppIcon(win.icon) && 'opacity-90 dark:opacity-100 dark:invert',
826
+ isAppIcon(icon) && 'opacity-90 dark:opacity-100 dark:invert',
794
827
  )}
795
828
  />
796
829
  ) : (
797
830
  <span className="h-4 w-4 shrink-0 rounded-sm bg-muted" />
798
831
  )}
799
- <span className="text-xs truncate">{windowLabel}</span>
832
+ <span className="text-xs truncate max-w-[100px]">{label}</span>
800
833
  </button>
801
834
  );
802
835
  })}
803
836
  </div>
837
+ )}
804
838
 
805
- {/* End navigation items (right side of taskbar) */}
806
- {endNavItems.length > 0 && (
807
- <div className="flex items-center gap-0.5 shrink-0 border-l border-sidebar-border pl-2 ml-1">
808
- {endNavItems.map((item) => {
809
- const label =
810
- typeof item.label === 'string'
811
- ? item.label
812
- : resolveNavLabel(item.label, currentLanguage);
813
- const icon =
814
- item.icon ??
815
- (item.openIn === 'external' ? getExternalFaviconUrl(item.url) : null);
816
- return (
817
- <button
818
- key={item.path}
819
- type="button"
820
- onClick={() => handleNavClick(item)}
821
- className="flex items-center gap-2 h-8 px-2 rounded cursor-pointer text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
822
- title={label}
823
- >
824
- {icon ? (
825
- <img
826
- src={icon}
827
- alt=""
828
- className={cn(
829
- 'h-4 w-4 shrink-0 rounded-sm object-cover',
830
- isAppIcon(icon) && 'opacity-90 dark:opacity-100 dark:invert',
831
- )}
832
- />
833
- ) : (
834
- <span className="h-4 w-4 shrink-0 rounded-sm bg-muted" />
835
- )}
836
- <span className="text-xs truncate max-w-[100px]">{label}</span>
837
- </button>
838
- );
839
- })}
840
- </div>
841
- )}
842
-
843
- {/* Date and time (extreme bottom right, OS-style); uses region timezone from settings */}
844
- <div
845
- className="flex flex-col items-end justify-center shrink-0 px-3 py-1 text-sidebar-foreground border-l border-sidebar-border ml-1 min-w-0"
846
- style={{ paddingRight: 'max(0.75rem, env(safe-area-inset-right))' }}
847
- role="timer"
848
- aria-live="off"
849
- aria-label={new Intl.DateTimeFormat(currentLanguage, {
839
+ {/* Date and time (extreme bottom right, OS-style); uses region timezone from settings */}
840
+ <div
841
+ className="flex flex-col items-end justify-center shrink-0 px-3 py-1 text-sidebar-foreground border-l border-sidebar-border ml-1 min-w-0"
842
+ style={{ paddingRight: 'max(0.75rem, env(safe-area-inset-right))' }}
843
+ role="timer"
844
+ aria-live="off"
845
+ aria-label={new Intl.DateTimeFormat(currentLanguage, {
846
+ timeZone,
847
+ dateStyle: 'full',
848
+ timeStyle: 'medium',
849
+ }).format(now)}
850
+ >
851
+ <time
852
+ dateTime={now.toISOString()}
853
+ className="text-xs leading-tight tabular-nums whitespace-nowrap"
854
+ >
855
+ {new Intl.DateTimeFormat(currentLanguage, {
850
856
  timeZone,
851
- dateStyle: 'full',
852
- timeStyle: 'medium',
857
+ hour: '2-digit',
858
+ minute: '2-digit',
859
+ second: '2-digit',
853
860
  }).format(now)}
861
+ </time>
862
+ <time
863
+ dateTime={now.toISOString()}
864
+ className="text-[10px] leading-tight whitespace-nowrap text-sidebar-foreground/90"
854
865
  >
855
- <time
856
- dateTime={now.toISOString()}
857
- className="text-xs leading-tight tabular-nums whitespace-nowrap"
858
- >
859
- {new Intl.DateTimeFormat(currentLanguage, {
860
- timeZone,
861
- hour: '2-digit',
862
- minute: '2-digit',
863
- second: '2-digit',
864
- }).format(now)}
865
- </time>
866
- <time
867
- dateTime={now.toISOString()}
868
- className="text-[10px] leading-tight whitespace-nowrap text-sidebar-foreground/90"
869
- >
870
- {new Intl.DateTimeFormat(currentLanguage, {
871
- timeZone,
872
- weekday: 'short',
873
- day: 'numeric',
874
- month: 'short',
875
- }).format(now)}
876
- </time>
877
- </div>
866
+ {new Intl.DateTimeFormat(currentLanguage, {
867
+ timeZone,
868
+ weekday: 'short',
869
+ day: 'numeric',
870
+ month: 'short',
871
+ }).format(now)}
872
+ </time>
878
873
  </div>
879
- </OverlayShell>
880
- </LayoutProviders>
874
+ </div>
875
+ </>
881
876
  );
882
877
  }