Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
003f82f
feat(web): add shadcn v2 preview UI
YSamed Aug 12, 2026
03f65cd
feat(web): expand AppShell v2 experience
YSamed Aug 12, 2026
a1a2a46
docs: add AGENTS.md with v2 design direction
YSamed Aug 12, 2026
032f6c3
feat(web): refine v2 sidebar navigation structure
YSamed Aug 12, 2026
700648f
feat(web): revamp v2 projects and notifications UX
YSamed Aug 12, 2026
7d21cdb
docs: clarify v2 UI design guidance
YSamed Aug 12, 2026
24156da
feat(web): overhaul v2 work items and cycles
YSamed Aug 12, 2026
e07e74c
feat(web): revamp v2 work item creation and views UX
YSamed Aug 13, 2026
412bfcd
feat(web): revamp v2 page chrome and list toolbars
YSamed Aug 13, 2026
dfca9b9
feat(v2): add full settings pages and panels
YSamed Aug 13, 2026
ac61fd6
feat(web): add Turkish i18n and shadcn-app theming
YSamed Aug 13, 2026
d083026
feat(web): add v2 auth pages and shell
YSamed Aug 13, 2026
d4518fa
feat(web): add interface version preference and route mapping
YSamed Aug 13, 2026
76875be
style(web): use shadcn-v4 theme in AppShell v2 preview
YSamed Aug 13, 2026
8cbbbab
refactor(web): make v2 a preference on shared routes and isolate it u…
YSamed Aug 13, 2026
3bf3b0a
fix(web): localize activity feed and fix v2 borders
YSamed Aug 13, 2026
e4ec730
feat(web): add v2 page transitions and suspense fallback
YSamed Aug 13, 2026
c1252cb
fix(web): keep v2 filter and display popovers inside the viewport
YSamed Aug 13, 2026
b4f4050
Fix v2 popover menu scroll sizing
YSamed Aug 14, 2026
67a1ac3
Enhance v2 lists with persisted controls
YSamed Aug 14, 2026
3c2f1e3
chore(web): remove unused v2 mock data
YSamed Aug 14, 2026
0164b86
chore: ignore local run.sh and run.ps1 launch scripts
YSamed Aug 14, 2026
85d4ba5
Fix v2 preference restore race conditions
YSamed Aug 14, 2026
a7251fa
feat(v2): add accent color appearance controls
YSamed Aug 14, 2026
b239b51
Build v2 estimates, integrations, webhooks panels
YSamed Aug 16, 2026
d7f5b60
v2: add image picker & upload/cover dialogs
YSamed Aug 16, 2026
16b8f64
refactor(v2): redesign cycles page layout
YSamed Aug 16, 2026
f465ff6
Refactor v2 work-item UI into shared modules
YSamed Aug 17, 2026
b868e5e
fix(web): reconcile v2 interface with main and complete integration
YSamed Oct 12, 2026
1637f1d
fix(api): resolve duplicate migration versions and validate catalog
YSamed Oct 12, 2026
be6660a
fix(web): address v2 foundation review findings
YSamed Oct 13, 2026
7b0f5f4
fix(web): preserve API calendar dates across timezones
YSamed Oct 13, 2026
881f4be
fix(web): preserve issue load failures and enable retry
YSamed Oct 13, 2026
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
61 changes: 61 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Repository instructions for coding agents

Read and follow `CLAUDE.md` for the repository architecture, commands, and
contribution rules. The instructions below add the design direction for the
v2 web interface.

## V2 web design direction

These rules apply to everything under `apps/web/src/v2/**` — the whole v2
interface lives there and nothing else does:

- `apps/web/src/v2/pages/**` — one file per v1 page, same filename
- `apps/web/src/v2/components/**` — shadcn primitives in `ui/`, the v2 shell in `layout/`
- `apps/web/src/v2/{contexts,hooks,lib,styles}/**`

Nothing outside `src/v2/` may import from it (enforced by `no-restricted-imports`
in `apps/web/eslint.config.js`); `src/routes/` is the one exception, since it
mounts both interfaces. The reverse is allowed: v2 reuses v1's services, api
types and domain components.

Use these references when designing or implementing the v2 interface:

1. [shadcn/ui component documentation](https://ui.shadcn.com/docs/components)
is the primary reference for component APIs, composition, accessibility,
states, and usage patterns.
2. [shadcn/ui source repository](https://github.com/shadcn-ui/ui) is the
primary reference when the documentation is insufficient or the current
component implementation needs to be inspected.
3. [AdminCN Free](https://shadcnstudio.com/templates/admin-dashboard/admincn-free)
is the visual and interaction reference for dashboard composition,
navigation, responsive layouts, tables, forms, settings, and application
pages.

### Implementation rules

- Prefer the project's existing shadcn primitives from
`@/components/shadcn/ui`. Their aliases and the `new-york` style are defined
in `apps/web/components.json`.
- Before creating a custom primitive, check the shadcn/ui documentation and
the components already present in `apps/web/src/components/shadcn/ui`.
- Use AdminCN as design inspiration, not as a framework dependency. Devlane is
a React 19 + Vite + React Router application; do not copy Next.js-specific
routing, server-component, or App Router patterns into it.
- Adapt external examples to Devlane's existing services, route conventions,
theme tokens, and component structure. Do not clone, vendor, or import an
external template wholesale.
- Keep v2 work within the v2 surface unless the task explicitly requires a
shared or legacy-interface change.
- Preserve the current shadcn token bridge and theme behavior. Prefer semantic
theme classes and tokens over one-off colors, radii, or spacing values.
- Treat `.shadcn-v4` and `.shadcn-reference` themes as comparison aids only;
shipped Devlane UI must use the normal Devlane token bridge.
- Every screen must remain responsive and accessible, including keyboard
navigation, visible focus, labels, loading/empty/error states, and usable
light and dark themes.
- Reuse components and patterns across v2 pages instead of duplicating the
same UI for individual pages.

When a reference conflicts with the repository's architecture or product
behavior, preserve Devlane's behavior and adapt only the visual/compositional
pattern.
40 changes: 40 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,46 @@ src/
- **Editor**: TipTap 3.22.3 — keep all `@tiptap/*` packages on the **same exact version** (they share peer deps; mismatches break silently).
- **Lazy boundaries**: every page is `lazy()` with a `<Suspense fallback={<PageFallback />}>`. New pages should follow the same pattern in `routes/index.tsx`.

## V2 interface design system

The v2 web interface lives entirely under `apps/web/src/v2/**` and is built on
shadcn/ui. **Read [AGENTS.md](AGENTS.md) before designing or implementing
anything on that surface** — it is the authoritative design direction and
lists the reference sources:

- [shadcn/ui docs](https://ui.shadcn.com/docs/components) — primary reference for component APIs, composition, accessibility, and states.
- [shadcn-ui/ui repo](https://github.com/shadcn-ui/ui) — source of truth when the docs are insufficient.
- [AdminCN Free](https://shadcnstudio.com/templates/admin-dashboard/admincn-free) — visual/interaction reference for dashboard composition, navigation, tables, forms, and settings pages.

Component config lives in `apps/web/components.json` (`new-york` style, slate
base, lucide icons, `@/v2/components/ui` alias). Reuse existing primitives
from that directory before adding new ones, and adapt references to Devlane's
services, routes, and theme tokens rather than vendoring template code.

**v2 is a preference, not a route.** Both interfaces answer to the same URLs.
`routes/index.tsx` declares each path once and picks the element with
`<Variant v1={…} v2={…} />` (`routes/InterfaceVariant.tsx`), which reads the
stored preference from `v2/contexts/InterfaceContext` (localStorage key
`devlane-interface-version`). Consequences when adding a page:

- Add one route entry, not two. If only one interface has the page, use plain
`<Suspense>` (setup and instance-admin are v1-only).
- v2 links must use the shipped v1 URL shapes (`/projects/:id/issues`, not
`work-items`); a v2 page reached by a v1 URL is the whole point.
- Both shells (`components/layout/AppShell.tsx` and its v2 mirror) mount on the
`:workspaceSlug` route and render an `<Outlet />`, so child routes are shared.

**v1 and v2 are isolated.** `src/v2/` mirrors `src/`: a v2 page carries the
same filename and export name as the v1 page it replaces
(`v2/pages/IssueListPage.tsx` ↔ `pages/IssueListPage.tsx`); `routes/index.tsx`
aliases the v2 side to `…PageV2` locally. Outside `src/v2/` and `src/routes/`,
nothing may import from `src/v2` — ESLint fails the build if it does. v1 files
must stay byte-identical to what they'd be without v2; the only v2-aware line
in a v1 page is `<InterfaceSwitch />` in `pages/SettingsPage.tsx`, whose
implementation lives in `routes/InterfaceSwitch.tsx`. v2 styles are imported by
the v2 shell and auth shell, not by the global `index.css`, so a v1 reader
never downloads them.

## Working with this repo

- When adding an API endpoint, the change usually touches: `model/` (if new entity) → `store/` → `service/` → `handler/` → register in `router/router.go` → corresponding service in `apps/web/src/services/` → web consumer.
Expand Down
7 changes: 7 additions & 0 deletions apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,10 @@ shared `apiClient` in `src/api/client.ts` (`withCredentials: true` for cookie
sessions — don't create new axios instances). Routing is workspace-scoped
(`/:workspaceSlug/...`). See the repo-root `CLAUDE.md` for the full frontend
architecture overview and `CONTRIBUTING.md` for commit/PR conventions.

## V2 design system

The v2 interface (`src/pages/*V2.tsx`, `src/components/shadcn/**`) is built on
shadcn/ui — `new-york` style, slate base color, lucide icons, configured in
`components.json`. The reference sources and the rules for using them are in
the repo-root `AGENTS.md`; read it before adding or restyling v2 UI.
5 changes: 4 additions & 1 deletion apps/web/src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ProjectIconModal, ProjectIconDisplay } from '../components/ProjectIconM
import { getImageUrl } from '../lib/utils';
import { useAuth } from '../contexts/AuthContext';
import { useTheme, type ThemePreference } from '../contexts/ThemeContext';
import { InterfaceSwitch } from '../routes/InterfaceSwitch';
import { workspaceService } from '../services/workspaceService';
import { ProjectNetworkSelect } from '../components/ProjectNetworkSelect';
import { projectService } from '../services/projectService';
Expand Down Expand Up @@ -71,6 +72,7 @@ import { InviteModal } from '../components/settings/modals/InviteModal';
import { ProjectStateModal } from '../components/settings/modals/ProjectStateModal';
import { ProjectLabelModal } from '../components/settings/modals/ProjectLabelModal';
import { formatRelativeTime, getTimezoneOptions } from '../lib/settingsHelpers';
import { describeActivity } from '../lib/activityDescription';

const COMPANY_SIZES = ['1-10', '11-50', '51-200', '201-500', '500+'];
export function SettingsPage() {
Expand Down Expand Up @@ -1372,6 +1374,7 @@ export function SettingsPage() {
})}
</div>
</div>
<InterfaceSwitch />
<div>
<label className="block text-sm font-medium text-(--txt-primary)">
{t('settings.preferences.firstDayOfWeek.title', 'First day of the week')}
Expand Down Expand Up @@ -1715,7 +1718,7 @@ export function SettingsPage() {
</p>
{a.description && (
<p className="mt-1 text-sm font-medium text-(--txt-primary)">
{a.description}
{describeActivity(a)}
</p>
)}
{a.issue_id && a.issue_name && workspaceSlug && (
Expand Down
57 changes: 57 additions & 0 deletions apps/web/src/routes/InterfaceSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useTranslation } from 'react-i18next';
import { useInterfaceVersion, type InterfaceVersion } from '../v2/contexts/InterfaceContext';

/**
* The one piece of v2 that reaches into the v1 interface: the control that
* switches between them. It lives here, in the routes seam, rather than in
* `pages/SettingsPage.tsx`, so the v1 page holds a single import and no v2
* knowledge — and so the v1 lint boundary against `src/v2` stays intact.
*
* Styled with Devlane's own tokens, not shadcn: it renders inside v1 chrome.
* Switching only writes the preference — both interfaces answer to the same
* URLs, so the page re-renders in place with no navigation.
*/
export function InterfaceSwitch() {
const { t } = useTranslation();
const { interfaceVersion, setInterfaceVersion } = useInterfaceVersion();

return (
<div>
<label className="block text-sm font-medium text-(--txt-primary)">
{t('settings.preferences.interfaceVersion.title', 'Interface')}
</label>
<p className="mt-0.5 text-sm text-(--txt-secondary)">
{t(
'settings.preferences.interfaceVersion.help',
'Choose which Devlane interface you want to use. Some admin-only pages still open in the classic interface either way.',
)}
</p>
<div className="mt-3 flex flex-wrap gap-2">
{(
[
['v1', t('settings.preferences.interfaceVersion.v1', 'Classic')],
['v2', t('settings.preferences.interfaceVersion.v2', 'New (Beta)')],
] as [InterfaceVersion, string][]
).map(([value, label]) => {
const selected = interfaceVersion === value;
return (
<button
key={value}
type="button"
onClick={() => setInterfaceVersion(value)}
aria-pressed={selected}
className={[
'rounded-(--radius-md) border px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-(--brand-default)',
selected
? 'border-(--brand-default) bg-(--bg-accent-subtle) text-(--txt-accent-primary)'
: 'border-(--border-subtle) text-(--txt-secondary) hover:border-(--border-strong-1)',
].join(' ')}
>
{label}
</button>
);
})}
</div>
</div>
);
}
32 changes: 32 additions & 0 deletions apps/web/src/routes/InterfaceVariant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Suspense, type ReactNode } from 'react';
import { PageFallback as PageFallbackV2 } from '../v2/components/page-fallback';
import { useInterfaceVersion } from '../v2/contexts/InterfaceContext';
import { PageFallback } from './PageFallback';

interface VariantProps {
/** Rendered when the interface preference is 'v1' (the default). */
v1: ReactNode;
/** Rendered when the interface preference is 'v2'. */
v2: ReactNode;
}

/**
* Renders one of two elements for the same URL, picked by the stored interface
* preference. This is what makes v2 a preference rather than a separate route
* tree: every path in `routes/index.tsx` is declared once, and only the element
* differs between the two interfaces.
*
* Both elements are constructed on every render, but constructing an element is
* just an object — the lazy component behind it is only imported when React
* actually renders it, so a v1 user never downloads a v2 chunk (and vice versa).
* The Suspense boundary lives here so each route entry stays a one-liner. Its
* fallback follows the preference too: each interface holds the page's shape in
* its own idiom while the chunk loads.
*/
export function Variant({ v1, v2 }: VariantProps) {
const { interfaceVersion } = useInterfaceVersion();
const isV2 = interfaceVersion === 'v2';
return (
<Suspense fallback={isV2 ? <PageFallbackV2 /> : <PageFallback />}>{isV2 ? v2 : v1}</Suspense>
);
}
11 changes: 11 additions & 0 deletions apps/web/src/routes/PageFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useTranslation } from 'react-i18next';

/** Suspense fallback for the lazily-loaded page components in the route tree. */
export function PageFallback() {
const { t } = useTranslation();
return (
<div className="flex items-center justify-center p-8 text-sm text-(--txt-tertiary)">
{t('common.loading', 'Loading…')}
</div>
);
}
Loading