Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ docs/
main.exe
.playwright-mcp/
.claude/
graphify-out/
graphify-out/
# Local dev launch scripts (machine-specific, not part of the repo)
run.sh
run.ps1
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
39 changes: 26 additions & 13 deletions apps/api/internal/handler/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,32 @@ func (h *UserHandler) GetActivity(c *gin.Context) {
issueID = a.IssueID.String()
issueName = issueMap[issueID]
}
activities = append(activities, userActivityItem{
createdAt: a.CreatedAt,
body: gin.H{
"id": a.ID.String(),
"type": "issue_activity",
"created_at": a.CreatedAt,
"description": issueActivityDescription(a),
"issue_id": issueID,
"issue_name": issueName,
"workspace_id": a.WorkspaceID.String(),
"project_id": a.ProjectID.String(),
},
})
body := gin.H{
"id": a.ID.String(),
"type": "issue_activity",
"created_at": a.CreatedAt,
"description": issueActivityDescription(a),
"issue_id": issueID,
"issue_name": issueName,
"workspace_id": a.WorkspaceID.String(),
"project_id": a.ProjectID.String(),
}
// The row's own comment is free text, so `description` is the only thing
// to show for it. Everything else is bookkeeping the client can phrase
// itself — send the parts so it can render them in the user's language.
if a.Comment == nil || strings.TrimSpace(*a.Comment) == "" {
body["verb"] = a.Verb
if a.Field != nil {
body["field"] = *a.Field
}
if a.OldValue != nil {
body["old_value"] = *a.OldValue
}
if a.NewValue != nil {
body["new_value"] = *a.NewValue
}
}
activities = append(activities, userActivityItem{createdAt: a.CreatedAt, body: body})
}
sort.SliceStable(activities, func(i, j int) bool {
return activities[i].createdAt.After(activities[j].createdAt)
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.
24 changes: 24 additions & 0 deletions apps/web/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/v2/components",
"utils": "@/lib/utils",
"ui": "@/v2/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@componentry": "https://componentry.fun/r/{name}.json"
}
}
41 changes: 40 additions & 1 deletion apps/web/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,51 @@ export default defineConfig([
'i18next/no-literal-string': 'off',
},
},
{
// Vendored shadcn/ui primitives (`npx shadcn add …`). These are upstream
// files we re-pull rather than hand-author, so the rules that assume our
// own authoring conventions are relaxed here — keeping the directory
// diff-clean against upstream. Anything we write ourselves lives outside
// this path and is still fully linted.
files: ['src/v2/components/**/*.{ts,tsx}'],
rules: {
// Upstream co-locates cva variant objects with the component.
'react-refresh/only-export-components': 'off',
// Upstream ships English a11y labels ("Close", "Previous"); those get
// translated at the call site or by wrapping the primitive, not by
// editing the vendored file.
'i18next/no-literal-string': 'off',
},
},
{
// The v2 interface is self-contained: everything it owns lives under
// `src/v2/`, and nothing outside may reach into it. `src/routes/` is the
// one exception — it is the composition root that mounts both interfaces
// on the same paths via <Variant>. The reverse direction stays open: v2
// reuses v1's services, contexts and domain components deliberately.
files: ['src/**/*.{ts,tsx}'],
ignores: ['src/v2/**', 'src/routes/**'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/v2/**', '@/v2/*'],
message:
'v1 code must not import from src/v2. The v2 interface is isolated; it is mounted from src/routes/index.tsx with <Variant>.',
},
],
},
],
},
},
{
// Guard against new hardcoded user-facing strings: flag literal JSX text so
// every visible string goes through t()/<Trans>. The i18n setup itself and
// tests are exempt.
files: ['src/**/*.tsx'],
ignores: ['src/i18n/**', '**/*.test.tsx'],
ignores: ['src/i18n/**', '**/*.test.tsx', 'src/v2/components/**'],
rules: {
'i18next/no-literal-string': [
'error',
Expand Down
5 changes: 3 additions & 2 deletions apps/web/i18next-parser.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
module.exports = {
input: ['src/**/*.{ts,tsx}'],
output: 'src/i18n/locales/$LOCALE/$NAMESPACE.json',
locales: ['en', 'az'],
locales: ['en', 'az', 'tr'],
defaultNamespace: 'translation',
// Match the runtime config: flat, dotted keys.
keySeparator: false,
Expand All @@ -16,6 +16,7 @@ module.exports = {
keepRemoved: true,
createOldCatalogs: false,
// For a new/untranslated key, seed the value with the key's default (English
// text passed as the second t() arg). az values are filled in by translators.
// text passed as the second t() arg). Other locale values are filled in by
// translators.
defaultValue: (locale, _ns, key, value) => (locale === 'en' ? (value ?? key) : ''),
};
Loading
Loading