Skip to content
Draft
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
26 changes: 26 additions & 0 deletions frontend/web/components/base/BareButton.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Reset for `<button>` primitives that need keyboard / screen-reader
// semantics but don't want the project's `.btn` chrome. Composed with
// utility classes for layout + appearance at the call site.
.bare-button {
appearance: none;
background: transparent;
border: 0;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
text-align: inherit;
cursor: pointer;

&:disabled {
cursor: not-allowed;
opacity: 0.6;
}

// Accessible focus default. Consumers can override with their own
// focus-visible rules if they need a different indicator.
&:focus-visible {
outline: 2px solid var(--color-border-action);
outline-offset: 2px;
}
}
30 changes: 30 additions & 0 deletions frontend/web/components/base/BareButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { ButtonHTMLAttributes, forwardRef } from 'react'
import './BareButton.scss'

type BareButtonProps = ButtonHTMLAttributes<HTMLButtonElement>

/**
* A button with no default chrome — just a clickable, keyboard-accessible
* surface. Use when you need a `<button>` for accessibility (keyboard
* activation, focus, screen-reader semantics) but the visual design is
* a styled custom shape — e.g. card rows, custom radios, icon-only
* triggers — that shouldn't inherit the project's `.btn` styling.
*
* Defaults `type='button'` so it never accidentally submits a parent
* form. Provides a `bare-button` reset class plus a default
* `:focus-visible` outline so keyboard users see where they are.
*/
const BareButton = forwardRef<HTMLButtonElement, BareButtonProps>(
({ className, type = 'button', ...rest }, ref) => (
<button
ref={ref}
type={type}
className={`bare-button ${className ?? ''}`.trim()}
{...rest}
/>
),
)

BareButton.displayName = 'BareButton'

export default BareButton
28 changes: 22 additions & 6 deletions frontend/web/components/navigation/navbars/TopNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Icon from 'components/icons/Icon'
import Headway from 'components/Headway'
import { Project } from 'common/types/responses'
import AccountDropdown from 'components/navigation/AccountDropdown'
import OnboardingChipWithDrawer from 'web/components/onboarding-quickstart/OnboardingChipWithDrawer'

type TopNavType = {
activeProject: Project | undefined
Expand All @@ -25,15 +26,30 @@ const TopNavbar: FC<TopNavType> = ({ activeProject, projectId }) => {
<div className='me-3'>
<GithubStar />
</div>
{/* TEMP: forced on for local validation. Restore the flag check
(Utils.getFlagsmithHasFeature('onboarding_quickstart_flow')) before merge.
Original NavLink left below in a comment for easy revert.
<NavLink
activeClassName='active'
to={'/getting-started'}
className='d-flex gap-1 d-none d-md-flex text-end lh-1 align-items-center'
>
<span>
<Icon name='rocket' width={20} fill='#9DA4AE' />
</span>
<span className='d-none d-md-block'>Getting Started</span>
</NavLink>
*/}
<OnboardingChipWithDrawer />
{/* TEMP: test-only quick link to the AHA onboarding flow.
Remove before merge along with the FORCE_ON gates. */}
<NavLink
activeClassName='active'
to={'/getting-started'}
className='d-flex gap-1 d-none d-md-flex text-end lh-1 align-items-center'
className='d-flex gap-1 ps-3 text-end lh-1 align-items-center text-warning'
title='Open onboarding flow (test only)'
>
<span>
<Icon name='rocket' width={20} fill='#9DA4AE' />
</span>
<span className='d-none d-md-block'>Getting Started</span>
<Icon name='rocket' width={16} />
<span className='d-none d-md-block'>Test onboarding</span>
</NavLink>
<a
className='d-flex gap-1 ps-3 text-end lh-1 align-items-center'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Slide-in concept-learning drawer. Single-mode; auto-dismissed by the
// chip when all items are complete. Tokens drive colour; this file owns
// the slide-in transform, backdrop fade, progress bar geometry, and the
// progress-fill width via a CSS custom property.

.concept-drawer {
position: fixed;
inset: 0 0 0 auto;
width: min(360px, 100vw);
z-index: 1050;
transform: translateX(100%);
transition: transform var(--duration-normal) var(--easing-standard);
border-left: 1px solid var(--color-border-default);
display: flex;
flex-direction: column;

&--open {
transform: translateX(0);
}

&__backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.12);
z-index: 1040;
opacity: 0;
pointer-events: none;
transition: opacity var(--duration-normal) var(--easing-standard);

&--open {
opacity: 1;
pointer-events: auto;
}
}

&__items {
overflow-y: auto;
flex: 1;
}

&__bar {
height: 6px;
overflow: hidden;
}

&__bar-fill {
height: 100%;
width: var(--concept-drawer-progress, 0%);
transition: width var(--duration-normal) var(--easing-standard);
}

&__marker {
display: inline-flex;
width: 22px;
height: 22px;
align-items: center;
justify-content: center;
}

&__marker-empty {
width: 14px;
height: 14px;
border-radius: 9999px;
border: 1.5px solid var(--color-border-strong);
}

&__item {
cursor: pointer;
transition: background-color var(--duration-fast) var(--easing-standard),
border-color var(--duration-fast) var(--easing-standard);

&:disabled {
cursor: default;
opacity: 0.7;
}
}
}

@media (prefers-reduced-motion: reduce) {
.concept-drawer,
.concept-drawer__backdrop,
.concept-drawer__bar-fill,
.concept-drawer__item {
transition: none;
}
}
142 changes: 142 additions & 0 deletions frontend/web/components/onboarding-quickstart/ConceptDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React, { FC, useEffect } from 'react'
import Icon from 'components/icons/Icon'
import {
CONCEPT_ITEMS,
ConceptItemId,
} from 'web/components/pages/onboarding-quickstart/data/presets'
import './ConceptDrawer.scss'

type ConceptDrawerProps = {
activeId: ConceptItemId | null
completedIds: ConceptItemId[]
isOpen: boolean
onClose: () => void
onItemClick: (id: ConceptItemId) => void
}

const ConceptDrawer: FC<ConceptDrawerProps> = ({
activeId,
completedIds,
isOpen,
onClose,
onItemClick,
}) => {
useEffect(() => {
if (!isOpen) return
const handler = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose()
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [isOpen, onClose])

const completedCount = completedIds.length
const totalCount = CONCEPT_ITEMS.length

return (
<>
<div
className={`concept-drawer__backdrop ${
isOpen ? 'concept-drawer__backdrop--open' : ''
}`}
onClick={onClose}
aria-hidden='true'
/>
<aside
className={`concept-drawer bg-surface-default border-default shadow-lg ${
isOpen ? 'concept-drawer--open' : ''
}`}
role='dialog'
aria-label='Get started'
aria-hidden={!isOpen}
>
<header className='concept-drawer__header d-flex align-items-center justify-content-between p-3 border-bottom border-default'>
<h3 className='mb-0'>Get started</h3>
<button
type='button'
className='btn btn-link p-0 text-muted'
onClick={onClose}
aria-label='Close'
>
<Icon name='close' width={20} />
</button>
</header>

<div className='concept-drawer__progress px-3 pt-3'>
<div className='d-flex align-items-center justify-content-between mb-2'>
<span className='text-muted'>Progress</span>
<span className='text-muted'>
{completedCount}/{totalCount}
</span>
</div>
<div className='concept-drawer__bar bg-surface-muted rounded-full'>
<div
className='concept-drawer__bar-fill bg-surface-action rounded-full'
style={
{
// CSS custom property used for width so we keep styling
// out of inline style declarations elsewhere.
'--concept-drawer-progress': `${
(completedCount / totalCount) * 100
}%`,
} as React.CSSProperties
}
/>
</div>
</div>

<ul className='concept-drawer__items list-unstyled m-0 p-3 d-flex flex-column gap-2'>
{CONCEPT_ITEMS.map((item) => {
const isCompleted = completedIds.includes(item.id)
const isActive = activeId === item.id && !isCompleted
return (
<li key={item.id}>
<button
type='button'
onClick={() => onItemClick(item.id)}
className={`concept-drawer__item w-100 text-start p-3 rounded-md border ${
isActive
? 'concept-drawer__item--active border-action bg-surface-action-subtle'
: 'border-default bg-surface-default'
}`}
disabled={isCompleted}
>
<div className='d-flex align-items-start gap-2'>
<span
className={`concept-drawer__marker ${
isCompleted
? 'concept-drawer__marker--done icon-success'
: ''
}`}
aria-hidden='true'
>
{isCompleted ? (
<Icon name='checkmark' width={18} />
) : (
<span className='concept-drawer__marker-empty' />
)}
</span>
<span className='flex-1'>
<span
className={`d-block fw-semibold ${
isCompleted ? 'text-muted' : 'text-default'
}`}
>
{item.title}
</span>
<span className='d-block text-muted'>
{item.description}
</span>
</span>
</div>
</button>
</li>
)
})}
</ul>
</aside>
</>
)
}

export default ConceptDrawer
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Header chip — idle + pulsing states. Lives in the top navbar slot.
// Colour comes from semantic tokens; this file owns only motion + geometry.

.onboarding-chip {
background: var(--color-surface-default);
cursor: pointer;
transition: background-color var(--duration-fast) var(--easing-standard);

&:hover {
background-color: var(--color-surface-hover);
}

&__dot {
width: 8px;
height: 8px;
border-radius: 9999px;
background-color: var(--color-icon-action);
position: relative;
display: inline-block;

&--pulsing {
&::before,
&::after {
content: '';
position: absolute;
inset: 0;
border-radius: 9999px;
background-color: currentColor;
opacity: 0.28;
animation: onboarding-chip-pulse 2s ease-out infinite;
}

&::after {
animation-delay: 1s;
}
}
}

&__counter {
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
}

@keyframes onboarding-chip-pulse {
0% {
transform: scale(1);
opacity: 0.28;
}
100% {
transform: scale(2.4);
opacity: 0;
}
}

@media (prefers-reduced-motion: reduce) {
.onboarding-chip__dot--pulsing {
&::before,
&::after {
animation: none;
opacity: 0;
}
}
}
Loading
Loading