initial ux ui improvements and revised dashboard

This commit is contained in:
Richard Osborne
2025-12-31 09:34:27 +01:00
parent ae7d3b8a8b
commit 73b5a42122
109 changed files with 13583 additions and 1111 deletions

View File

@@ -22,7 +22,7 @@
.Title {
font-family: var(--font-family);
color: #aaa;
color: var(--theme-color-fg-muted);
font-size: 12px;
position: absolute;
left: 50%;
@@ -31,7 +31,7 @@
.Version {
font-family: var(--font-family);
color: #c4c4c4;
color: var(--theme-color-fg-default-shy);
font-size: 11px;
margin-right: 10px;
}

View File

@@ -49,8 +49,8 @@
}
.language-javascript {
color: #eee;
background-color: #444;
color: var(--theme-color-fg-highlight);
background-color: var(--theme-color-bg-4);
display: block;
padding: 5px;
overflow: scroll;

View File

@@ -2,20 +2,25 @@
.Root {
border: 0;
font-size: 14px;
font-weight: var(--font-weight-semibold);
padding-left: 12px;
padding-right: 12px;
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-tight);
padding: var(--spacing-button-padding-y) var(--spacing-button-padding-x);
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-button-gap);
cursor: pointer;
min-height: length.$global-input-size;
min-width: 70px;
flex-grow: 0;
flex-shrink: 0;
position: relative;
transition: background-color var(--speed-turbo) var(--easing-base), color var(--speed-turbo) var(--easing-base);
border-radius: var(--radius-md);
transition: background-color var(--transition-default) var(--transition-ease),
color var(--transition-default) var(--transition-ease),
border-color var(--transition-default) var(--transition-ease),
box-shadow var(--transition-default) var(--transition-ease), transform var(--transition-fast) var(--transition-ease);
&.is-size-small {
min-height: length.$global-input-size-small;
@@ -25,13 +30,22 @@
&.is-variant-cta {
color: var(--theme-color-on-primary);
background-color: var(--theme-color-primary);
box-shadow: var(--shadow-sm);
path {
fill: var(--theme-color-on-primary);
}
&:hover {
&:hover:not(:disabled) {
background-color: var(--theme-color-primary-highlight);
box-shadow: var(--shadow-default);
transform: translateY(-1px);
}
&:active:not(:disabled) {
background-color: var(--theme-color-primary-dim);
box-shadow: none;
transform: translateY(0);
}
}
@@ -105,14 +119,22 @@
&:disabled {
cursor: not-allowed;
opacity: 0.5;
background-color: var(--theme-color-bg-3) !important;
color: var(--theme-color-fg-muted);
transform: none !important;
box-shadow: none !important;
path {
fill: var(--theme-color-fg-muted);
}
}
&:focus-visible {
outline: 2px solid var(--theme-color-focus-ring);
outline-offset: 2px;
}
&.has-left-spacing {
margin-left: 12px;
}

View File

@@ -30,12 +30,16 @@
.InputArea {
height: length.$global-input-size;
padding: 4px 5px;
padding: var(--spacing-input-padding-y) var(--spacing-input-padding-x);
position: relative;
display: flex;
cursor: text;
border: 1px solid var(--theme-color-border-default);
border-radius: var(--radius-default);
transition: margin-bottom var(--speed-quick) var(--easing-base),
background-color var(--speed-quick) var(--easing-base);
background-color var(--transition-default) var(--transition-ease),
border-color var(--transition-default) var(--transition-ease),
box-shadow var(--transition-default) var(--transition-ease);
.Root.is-variant-default & {
background-color: var(--theme-color-bg-3);
@@ -44,8 +48,15 @@
background-color: var(--theme-color-bg-4);
}
&:hover:not(.is-focused):not(.is-readonly) {
border-color: var(--theme-color-border-strong);
}
&.is-focused {
background-color: var(--theme-color-bg-1);
border-color: var(--theme-color-focus-ring);
box-shadow: 0 0 0 2px rgba(210, 31, 60, 0.15);
outline: none;
}
}

View File

@@ -11,6 +11,11 @@
&.has-backdrop {
background-color: var(--theme-color-bg-1-transparent);
/* Optional: Subtle blur for modern feel */
/* Note: May have performance implications on older hardware */
/* backdrop-filter: blur(4px); */
/* -webkit-backdrop-filter: blur(4px); */
}
&.is-locking-scroll {
@@ -23,8 +28,22 @@
}
.VisibleDialog {
filter: drop-shadow(0 4px 15px var(--theme-color-bg-1-transparent-2));
box-shadow: 0 0 10px -5px var(--theme-color-bg-1-transparent-2);
/* Modern elevated shadow */
box-shadow: var(--shadow-popup);
/* Border for definition against backdrop */
border: 1px solid var(--theme-color-border-subtle);
/* Modern rounded corners */
border-radius: var(--radius-lg);
/* Overflow handling */
overflow: hidden;
/* Size constraints */
max-height: 90vh;
max-width: 90vw;
position: absolute;
width: var(--width);
pointer-events: all;
@@ -53,10 +72,28 @@
right: 0;
bottom: 0;
background-color: var(--background);
border-radius: 2px;
border-radius: var(--radius-lg);
overflow: hidden;
pointer-events: none; // Allow clicks to pass through to content
}
/* Custom scrollbar styling for dialog content */
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: var(--theme-color-bg-3);
}
&::-webkit-scrollbar-thumb {
background: var(--theme-color-bg-5);
border-radius: var(--radius-full);
&:hover {
background: var(--theme-color-fg-muted);
}
}
}
.Arrow {

View File

@@ -3,6 +3,16 @@
display: flex;
flex-direction: column;
background-color: var(--theme-color-bg-4);
/* Modern rounded corners */
border-radius: var(--radius-lg);
/* Border for definition */
border: 1px solid var(--theme-color-border-subtle);
/* Elevated shadow */
box-shadow: var(--shadow-popup);
max-width: 810px;
max-height: 90vh;
width: 80vw;
@@ -22,40 +32,58 @@
.CloseButtonContainer {
position: absolute;
top: 8px;
right: 8px;
top: var(--spacing-2);
right: var(--spacing-2);
}
.Header {
padding: 20px 40px 16px;
padding: var(--spacing-5) var(--spacing-10) var(--spacing-4);
display: flex;
justify-content: space-between;
align-items: flex-start;
&.has-divider {
border-bottom: 1px solid var(--theme-color-bg-3);
border-bottom: 1px solid var(--theme-color-border-subtle);
}
}
.Footer {
padding: 20px 40px 16px;
padding: var(--spacing-5) var(--spacing-10) var(--spacing-4);
display: flex;
justify-content: space-between;
align-items: flex-start;
&.has-divider {
border-top: 1px solid var(--theme-color-bg-3);
border-top: 1px solid var(--theme-color-border-subtle);
}
}
.TitleWrapper {
padding-top: 20px;
padding-right: 40px;
padding-top: var(--spacing-5);
padding-right: var(--spacing-10);
}
.Content {
padding: 0 40px 40px;
padding-top: 16px;
padding: 0 var(--spacing-10) var(--spacing-10);
padding-top: var(--spacing-4);
overflow-x: hidden;
overflow-y: overlay;
/* Custom scrollbar styling */
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: var(--theme-color-bg-3);
}
&::-webkit-scrollbar-thumb {
background: var(--theme-color-bg-5);
border-radius: var(--radius-full);
&:hover {
background: var(--theme-color-fg-muted);
}
}
}

View File

@@ -0,0 +1,121 @@
/**
* TabBar - Modern, clean horizontal tab navigation
*/
.Root {
display: flex;
align-items: center;
gap: 4px;
padding: 0;
margin: 0;
background: transparent;
border-bottom: 1px solid var(--theme-color-border-default);
}
.Tab {
position: relative;
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: transparent;
border: none;
cursor: pointer;
color: var(--theme-color-fg-default-shy);
font-size: 14px;
font-weight: 500;
line-height: 1;
transition: all 0.2s ease;
white-space: nowrap;
user-select: none;
// Bottom border indicator
&::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: transparent;
transition: background 0.2s ease;
}
&:hover:not(.is-disabled) {
color: var(--theme-color-fg-default);
background: var(--theme-color-bg-hover);
}
&:focus {
outline: none;
box-shadow: inset 0 0 0 2px var(--theme-color-primary);
}
&:focus:not(:focus-visible) {
box-shadow: none;
}
&.is-active {
color: var(--theme-color-primary);
font-weight: 600;
&::after {
background: var(--theme-color-primary);
}
}
&.is-disabled {
color: var(--theme-color-fg-disabled);
cursor: not-allowed;
opacity: 0.5;
}
}
.Tab-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.Tab-label {
display: flex;
align-items: center;
}
// Size variants
.is-size-small {
.Tab {
padding: 8px 16px;
font-size: 13px;
gap: 6px;
}
.Tab-icon {
font-size: 14px;
}
}
.is-size-medium {
.Tab {
padding: 12px 20px;
font-size: 14px;
gap: 8px;
}
.Tab-icon {
font-size: 16px;
}
}
.is-size-large {
.Tab {
padding: 16px 24px;
font-size: 15px;
gap: 10px;
}
.Tab-icon {
font-size: 18px;
}
}

View File

@@ -0,0 +1,115 @@
/**
* TabBar - Storybook Stories
*/
import { Meta, StoryObj } from '@storybook/react';
import React, { useState } from 'react';
import { IconName } from '@noodl-core-ui/components/common/Icon';
import { TabBar, TabBarItem } from './TabBar';
const meta: Meta<typeof TabBar> = {
title: 'Layout/TabBar',
component: TabBar,
parameters: {
layout: 'padded'
},
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof TabBar>;
// Basic tabs without icons
const basicItems: TabBarItem[] = [
{ id: 'projects', label: 'Projects' },
{ id: 'learn', label: 'Learn' },
{ id: 'templates', label: 'Templates' }
];
// Tabs with icons
const iconItems: TabBarItem[] = [
{ id: 'projects', label: 'Projects', icon: IconName.Folder },
{ id: 'learn', label: 'Learn', icon: IconName.Book },
{ id: 'templates', label: 'Templates', icon: IconName.Components }
];
// Tabs with disabled state
const disabledItems: TabBarItem[] = [
{ id: 'projects', label: 'Projects', icon: IconName.Folder },
{ id: 'learn', label: 'Learn', icon: IconName.Book },
{ id: 'templates', label: 'Templates', icon: IconName.Components, disabled: true },
{ id: 'marketplace', label: 'Marketplace', icon: IconName.Package, disabled: true }
];
// Interactive wrapper for stories
function TabBarDemo({ items, size }: { items: TabBarItem[]; size?: 'small' | 'medium' | 'large' }) {
const [activeId, setActiveId] = useState(items[0].id);
return (
<div>
<TabBar items={items} activeItemId={activeId} onChange={setActiveId} size={size} />
<div style={{ padding: '20px', color: 'var(--theme-color-fg-default)' }}>
<strong>Active Tab:</strong> {activeId}
</div>
</div>
);
}
export const Basic: Story = {
render: () => <TabBarDemo items={basicItems} />
};
export const WithIcons: Story = {
render: () => <TabBarDemo items={iconItems} />
};
export const WithDisabled: Story = {
render: () => <TabBarDemo items={disabledItems} />
};
export const SmallSize: Story = {
render: () => <TabBarDemo items={iconItems} size="small" />
};
export const MediumSize: Story = {
render: () => <TabBarDemo items={iconItems} size="medium" />
};
export const LargeSize: Story = {
render: () => <TabBarDemo items={iconItems} size="large" />
};
export const ManyTabs: Story = {
render: () => {
const manyItems: TabBarItem[] = [
{ id: '1', label: 'Projects', icon: IconName.Folder },
{ id: '2', label: 'Learn', icon: IconName.Book },
{ id: '3', label: 'Templates', icon: IconName.Components },
{ id: '4', label: 'Marketplace', icon: IconName.Package },
{ id: '5', label: 'Settings', icon: IconName.Settings },
{ id: '6', label: 'Help', icon: IconName.Question }
];
return <TabBarDemo items={manyItems} />;
}
};
export const KeyboardNavigation: Story = {
render: () => (
<div>
<TabBarDemo items={iconItems} />
<div style={{ padding: '20px', color: 'var(--theme-color-fg-default-shy)', fontSize: '13px' }}>
<p>
<strong>Keyboard shortcuts:</strong>
</p>
<ul>
<li>Arrow Left/Right: Navigate between tabs</li>
<li>Home: Go to first tab</li>
<li>End: Go to last tab</li>
<li>Tab: Focus next element</li>
</ul>
</div>
</div>
)
};

View File

@@ -0,0 +1,124 @@
/**
* TabBar - Modern horizontal tab navigation component
*
* A clean, accessible tab bar for switching between views.
* Supports keyboard navigation, icons, and state persistence.
*
* @module noodl-core-ui/components/layout
*/
import classNames from 'classnames';
import React, { useEffect, useRef } from 'react';
import { Icon, IconName } from '@noodl-core-ui/components/common/Icon';
import { UnsafeStyleProps } from '@noodl-core-ui/types/global';
import css from './TabBar.module.scss';
export interface TabBarItem {
id: string;
label: string;
icon?: IconName;
disabled?: boolean;
testId?: string;
}
export interface TabBarProps extends UnsafeStyleProps {
items: TabBarItem[];
activeItemId: string;
onChange: (itemId: string) => void;
/**
* Size variant for the tab bar
*/
size?: 'small' | 'medium' | 'large';
}
export function TabBar({
items,
activeItemId,
onChange,
size = 'medium',
UNSAFE_className,
UNSAFE_style
}: TabBarProps) {
const tabRefs = useRef<{ [key: string]: HTMLButtonElement | null }>({});
/**
* Handle keyboard navigation
*/
const handleKeyDown = (event: React.KeyboardEvent, currentIndex: number) => {
const enabledItems = items.filter((item) => !item.disabled);
const currentEnabledIndex = enabledItems.findIndex((item) => item.id === items[currentIndex].id);
let newIndex = currentEnabledIndex;
switch (event.key) {
case 'ArrowLeft':
event.preventDefault();
newIndex = currentEnabledIndex > 0 ? currentEnabledIndex - 1 : enabledItems.length - 1;
break;
case 'ArrowRight':
event.preventDefault();
newIndex = currentEnabledIndex < enabledItems.length - 1 ? currentEnabledIndex + 1 : 0;
break;
case 'Home':
event.preventDefault();
newIndex = 0;
break;
case 'End':
event.preventDefault();
newIndex = enabledItems.length - 1;
break;
default:
return;
}
const newItem = enabledItems[newIndex];
if (newItem) {
onChange(newItem.id);
tabRefs.current[newItem.id]?.focus();
}
};
return (
<div
className={classNames(css['Root'], css[`is-size-${size}`], UNSAFE_className)}
style={UNSAFE_style}
role="tablist"
aria-label="Main navigation"
>
{items.map((item, index) => {
const isActive = item.id === activeItemId;
const isDisabled = item.disabled;
return (
<button
key={item.id}
ref={(el) => {
tabRefs.current[item.id] = el;
}}
className={classNames(css['Tab'], {
[css['is-active']]: isActive,
[css['is-disabled']]: isDisabled
})}
onClick={() => !isDisabled && onChange(item.id)}
onKeyDown={(e) => handleKeyDown(e, index)}
disabled={isDisabled}
role="tab"
aria-selected={isActive}
aria-disabled={isDisabled}
tabIndex={isActive ? 0 : -1}
data-test={item.testId}
>
{item.icon && (
<span className={css['Tab-icon']}>
<Icon icon={item.icon} />
</span>
)}
<span className={css['Tab-label']}>{item.label}</span>
</button>
);
})}
</div>
);
}

View File

@@ -0,0 +1,2 @@
export { TabBar } from './TabBar';
export type { TabBarItem, TabBarProps } from './TabBar';

View File

@@ -1,37 +1,37 @@
.Root {
background-color: var(--theme-color-bg-4);
&.has-bottom-border {
border-bottom: 1px solid var(--theme-color-bg-4);
}
}
.Header {
padding: 8px 10px;
background-color: var(--theme-color-bg-3);
}
.Content {
padding: 10px;
display: flex;
flex-direction: column;
&.has-y-padding {
padding-top: 65px;
padding-bottom: 65px;
}
&.is-centering-children {
align-items: center;
}
}
.Title {
color: #ccc;
font-size: 11px;
text-transform: uppercase;
margin: 0;
padding: 0;
font-family: var(--font-family);
font-weight: var(--font-weight-semibold);
}
.Root {
background-color: var(--theme-color-bg-4);
&.has-bottom-border {
border-bottom: 1px solid var(--theme-color-bg-4);
}
}
.Header {
padding: 8px 10px;
background-color: var(--theme-color-bg-3);
}
.Content {
padding: 10px;
display: flex;
flex-direction: column;
&.has-y-padding {
padding-top: 65px;
padding-bottom: 65px;
}
&.is-centering-children {
align-items: center;
}
}
.Title {
color: var(--theme-color-fg-default-shy);
font-size: 11px;
text-transform: uppercase;
margin: 0;
padding: 0;
font-family: var(--font-family);
font-weight: var(--font-weight-semibold);
}

View File

@@ -1,7 +1,26 @@
.Root {
/* Typography */
font-family: var(--font-family);
padding: 14px;
max-width: 160px;
font-size: var(--font-size-sm);
line-height: var(--line-height-normal);
/* Background and styling */
background-color: var(--theme-color-bg-4);
color: var(--theme-color-fg-default);
/* Spacing */
padding: var(--spacing-3-5);
border-radius: var(--radius-default);
/* Elevated appearance */
box-shadow: var(--shadow-md);
border: 1px solid var(--theme-color-border-default);
/* Ensure tooltip is above everything */
z-index: var(--z-tooltip);
/* Size constraint */
max-width: 250px;
span {
text-align: center;
@@ -10,9 +29,11 @@
}
.FineType {
padding: 0 8px 8px;
margin-top: -6px;
padding: 0 var(--spacing-2) var(--spacing-2);
margin-top: calc(-1 * var(--spacing-1-5));
text-align: center;
font-size: var(--font-size-xs);
color: var(--theme-color-fg-default-shy);
}
.Trigger {

View File

@@ -2,6 +2,19 @@
display: flex;
flex-direction: column;
height: 100%;
/* Background */
background-color: var(--theme-color-bg-2);
/* Subtle border for definition */
border: 1px solid var(--theme-color-border-subtle);
border-radius: var(--radius-md);
/* Consistent padding */
padding: var(--spacing-panel-padding);
/* Panel gap between children */
gap: var(--spacing-panel-gap);
}
.Inner {
@@ -21,6 +34,24 @@
&.has-content-scroll {
overflow-y: overlay;
z-index: 0;
/* Custom scrollbar styling */
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: var(--theme-color-bg-3);
}
&::-webkit-scrollbar-thumb {
background: var(--theme-color-bg-5);
border-radius: var(--radius-full);
&:hover {
background: var(--theme-color-fg-muted);
}
}
}
&.is-fill {
@@ -36,12 +67,12 @@
z-index: -1;
content: '';
position: absolute;
top: 5px;
top: var(--spacing-1);
left: 0;
right: 0;
height: 10px;
border-radius: 50%;
box-shadow: 0 0 10px 15px var(--theme-color-bg-2);
box-shadow: var(--shadow-sm) var(--theme-color-bg-2);
pointer-events: none;
}
}

View File

@@ -44,7 +44,7 @@
/* consistency */
flex: 0 0 36px;
min-height: 36px;
padding: 0 10px 0 16px;
padding: 0 var(--spacing-2-5) 0 var(--spacing-4);
user-select: none;
@@ -63,13 +63,17 @@
&.is-collapsable {
padding-right: 0;
cursor: pointer;
&:hover {
background-color: var(--theme-color-bg-hover);
}
}
}
.Body {
/* allow scrolling */
overflow: hidden overlay;
padding-top: 8px;
padding-top: var(--spacing-2);
&.is-variant-in-modal {
padding: 0;
@@ -80,10 +84,28 @@
}
&.has-bottom-spacing {
padding-bottom: 12px;
padding-bottom: var(--spacing-3);
}
&.has-gutter {
padding: 15px;
padding: var(--spacing-section-padding);
}
/* Custom scrollbar styling */
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: var(--theme-color-bg-3);
}
&::-webkit-scrollbar-thumb {
background: var(--theme-color-bg-5);
border-radius: var(--radius-full);
&:hover {
background: var(--theme-color-fg-muted);
}
}
}

View File

@@ -8,11 +8,11 @@
align-items: center;
white-space: nowrap;
cursor: default;
&.actionable {
color: #9F9F9F !important;
color: var(--theme-color-fg-default) !important;
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.2);
}
@@ -21,12 +21,12 @@
.Text {
/* Color is slightly different in the popout */
color: #7a7a7a;
color: var(--theme-color-fg-muted);
/* TODO: Bug there Text doesnt have a font family */
font-family: 'OpenSans';
&.actionable {
color: #9F9F9F !important;
color: var(--theme-color-fg-default) !important;
}
}

View File

@@ -6,7 +6,7 @@
height: 100%;
cursor: nwse-resize;
fill: #7a7a7a;
fill: var(--theme-color-fg-muted);
display: flex;
justify-content: flex-end;