Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ and this project adheres to

### Added

- Add collapsible left sidebar with keyboard shortcut (Cmd/Ctrl+M) and
command-palette style project picker (Cmd/Ctrl+Shift+P)
[#4197](https://github.com/OpenFn/lightning/pull/4197)

### Changed

### Fixed
Expand Down
257 changes: 257 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,263 @@
}
}
}

/* Prevent icons from shrinking in flex containers */
.menu-item svg {
flex-shrink: 0;
}

/* Hide scrollbar in sidebar to prevent flash during transition */
& ::-webkit-scrollbar {
display: none;
}

& {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
}

/* Sidebar header (logo container + toggle) - can be inside or outside #side-menu */
.app-logo-container,
.sidebar-toggle-btn {
background-color: var(--primary-bg-dark);
color: var(--primary-text);

&.secondary-variant {
--primary-bg: var(--color-blue-700);
--primary-text: white;
--primary-bg-dark: var(--color-blue-900);
}

&.sudo-variant {
--primary-bg: var(--color-slate-700);
--primary-text: white;
--primary-bg-dark: var(--color-slate-900);
}
}

/* Material Design easing for sidebar transitions */
#sidebar-panel,
#sidebar-panel + div > div:first-child {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Menu item text - base styles with transition */
#sidebar-panel .menu-item-text {
transition: max-width 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
white-space: nowrap;
}

/* Logo text transition */
#sidebar-panel .sidebar-logo-text {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Sidebar footer - default state (expanded) */
#sidebar-panel .sidebar-footer {
.sidebar-logo-text {
max-width: 50px;
margin-right: 4px;
opacity: 1;
}

.sidebar-version-chip {
opacity: 0.5;
transition-delay: 200ms; /* Appear after menu animation */
}
}

/* Keep sidebar expanded when menu is open */
#sidebar-panel.sidebar-collapsed.menu-open {
width: 192px !important;
box-shadow: 8px 0 24px rgba(0, 0, 0, 0.3), 4px 0 8px rgba(0, 0, 0, 0.2);

&::after {
opacity: 1;
visibility: visible;
}

.menu-item-text {
max-width: 150px !important;
opacity: 1 !important;
}

/* Reset menu item alignment when menu open */
.menu-item {
justify-content: flex-start !important;
}

.menu-item svg {
margin-right: 0.5rem !important;
}

.project-picker-text,
.project-picker-chevron,
.user-menu-chevron,
.sidebar-expanded-logo {
display: block !important;
}

.user-menu-text {
display: flex !important;
}

.sidebar-logo-text {
max-width: 50px !important;
margin-right: 4px !important;
opacity: 1 !important;
}

.sidebar-version-chip {
opacity: 0.5 !important;
transition-delay: 200ms !important;
}

#project-picker-wrapper,
#user-menu-wrapper {
display: block !important;
}

#project-picker-trigger,
#user-menu-trigger {
width: 100% !important;
gap: 0.5rem !important;
}
}

/* Sidebar collapsed state - hide text */
#sidebar-panel.sidebar-collapsed {
transition: width 200ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1);

/* Hide text elements when collapsed - use opacity/width for smooth animation */
.menu-item-text {
max-width: 0 !important;
opacity: 0 !important;
}

/* Center icons when collapsed */
.menu-item {
justify-content: center;
}

.menu-item svg {
margin-right: 0 !important;
}

.project-picker-text,
.project-picker-chevron,
.user-menu-text,
.user-menu-chevron {
display: none !important;
}

/* Animate logo text to collapse */
.sidebar-logo-text {
max-width: 0 !important;
margin-right: 0 !important;
opacity: 0 !important;
}

/* Hide version chip when collapsed - keep space, just hide content */
.sidebar-version-chip {
opacity: 0 !important;
transition-delay: 0ms !important;
}

/* Center wrappers when collapsed */
#project-picker-wrapper,
#user-menu-wrapper {
display: flex !important;
justify-content: center !important;
}

/* Shrink to fit when collapsed */
#project-picker-trigger,
#user-menu-trigger {
width: auto !important;
gap: 0 !important;
}

/* Hover expansion - show everything, expand width */
/* Overlay when collapsed - matches standardized overlay style */
&::after {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgb(17 24 39 / 0.6); /* gray-900/60 */
backdrop-filter: blur(4px); /* backdrop-blur-sm */
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1), visibility 200ms cubic-bezier(0.4, 0, 0.2, 1);
z-index: -1;
}

&:hover {
width: 192px !important;
box-shadow: 8px 0 24px rgba(0, 0, 0, 0.3), 4px 0 8px rgba(0, 0, 0, 0.2);

&::after {
opacity: 1;
visibility: visible;
}

/* Show all text elements on hover */
.menu-item-text {
max-width: 150px !important;
opacity: 1 !important;
}

/* Reset menu item alignment on hover */
.menu-item {
justify-content: flex-start !important;
}

.menu-item svg {
margin-right: 0.5rem !important;
}

.project-picker-text,
.project-picker-chevron,
.user-menu-chevron {
display: block !important;
}

/* User menu button needs flex display */
.user-menu-text {
display: flex !important;
}

/* Animate logo text on hover */
.sidebar-logo-text {
max-width: 50px !important;
margin-right: 4px !important;
opacity: 1 !important;
}

/* Show version chip on hover - fades in after menu animation */
.sidebar-version-chip {
opacity: 0.5 !important;
transition-delay: 200ms !important;
}

/* Restore wrapper and button layout on hover */
#project-picker-wrapper,
#user-menu-wrapper {
display: block !important;
}

#project-picker-trigger,
#user-menu-trigger {
width: 100% !important;
gap: 0.5rem !important;
}
}
}

/* Alerts and form errors used by phx.new */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export function AdaptorSelectionModal({
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
<DialogBackdrop
transition
className="fixed inset-0 bg-gray-500/75 transition-opacity
className="fixed inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity
data-closed:opacity-0 data-enter:duration-300
data-enter:ease-out data-leave:duration-200
data-leave:ease-in"
Expand Down
2 changes: 1 addition & 1 deletion assets/js/collaborative-editor/components/AlertDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function AlertDialog({
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
<DialogBackdrop
transition
className="fixed inset-0 bg-gray-500/75 transition-opacity
className="fixed inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity
data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out
data-leave:duration-200 data-leave:ease-in"
/>
Expand Down
46 changes: 26 additions & 20 deletions assets/js/collaborative-editor/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,34 @@ import { cn } from '../../utils/cn';

export function Breadcrumbs({ children }: { children: React.ReactNode[] }) {
// Split: Last item is always the title (always visible)
// Of the remaining breadcrumbs, show only the last one, hide the rest
const { hiddenItems, visibleBreadcrumb, title } = useMemo(() => {
// Project name (2nd item) is always visible for context when sidebar is collapsed
// Other breadcrumbs go into the dropdown
const { hiddenItems, visibleBreadcrumbs, title } = useMemo(() => {
if (children.length === 0) {
return { hiddenItems: [], visibleBreadcrumb: null, title: null };
return { hiddenItems: [], visibleBreadcrumbs: [], title: null };
}

// Last child is the title
// Last child is the title (workflow name)
const titleItem = children[children.length - 1];
const breadcrumbs = children.slice(0, -1);

if (breadcrumbs.length > 1) {
// Hide all but the last breadcrumb
if (breadcrumbs.length >= 2) {
// Structure: [Projects, ProjectName, Workflows, ...]
// Hide only "Projects" (index 0) in the dropdown
// Show project name (index 1) and everything after as visible
const hiddenItems = breadcrumbs.slice(0, 1); // Only "Projects" link in dropdown
const visibleBreadcrumbs = breadcrumbs.slice(1); // ProjectName, Workflows, etc.

return {
hiddenItems: breadcrumbs.slice(0, -1),
visibleBreadcrumb: breadcrumbs[breadcrumbs.length - 1],
hiddenItems,
visibleBreadcrumbs,
title: titleItem,
};
}

return {
hiddenItems: [],
visibleBreadcrumb: breadcrumbs[0] ?? null,
visibleBreadcrumbs: breadcrumbs,
title: titleItem,
};
}, [children]);
Expand All @@ -39,28 +45,28 @@ export function Breadcrumbs({ children }: { children: React.ReactNode[] }) {
result.push(<BreadcrumbDropdown key="dropdown" items={hiddenItems} />);
}

// Add visible breadcrumb (if exists)
if (visibleBreadcrumb) {
// Only show separator if there are hidden items (ellipsis dropdown before this)
if (hiddenItems.length > 0) {
// Add visible breadcrumbs
visibleBreadcrumbs.forEach((breadcrumb, index) => {
// Add separator if there's something before this breadcrumb
if (hiddenItems.length > 0 || index > 0) {
result.push(
<span
key="chevron-breadcrumb"
key={`chevron-breadcrumb-${index}`}
className="hero-chevron-right-mini w-5 h-5 text-secondary-500"
/>
);
}
result.push(
<li key="visible-breadcrumb" className="flex items-center">
{visibleBreadcrumb}
<li key={`visible-breadcrumb-${index}`} className="flex items-center">
{breadcrumb}
</li>
);
}
});

// Add title (with separator only if there's something before it)
if (title) {
// Show separator if there are hidden items OR a visible breadcrumb
if (hiddenItems.length > 0 || visibleBreadcrumb !== null) {
// Show separator if there are hidden items OR visible breadcrumbs
if (hiddenItems.length > 0 || visibleBreadcrumbs.length > 0) {
result.push(
<span
key="chevron-title"
Expand All @@ -76,7 +82,7 @@ export function Breadcrumbs({ children }: { children: React.ReactNode[] }) {
}

return result;
}, [hiddenItems, visibleBreadcrumb, title]);
}, [hiddenItems, visibleBreadcrumbs, title]);

return (
<nav className="flex" aria-label="Breadcrumb">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export function ConfigureAdaptorModal({
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
<DialogBackdrop
transition
className="fixed inset-0 bg-black/30 transition-opacity
className="fixed inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity
data-closed:opacity-0 data-enter:duration-300
data-enter:ease-out data-leave:duration-200
data-leave:ease-in"
Expand Down
Loading