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
22 changes: 11 additions & 11 deletions packages/app/src/components/file-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from "solid-js"
import { Dynamic } from "solid-js/web"
import type { FileNode } from "@opencode-ai/sdk/v2"
import { FirlawFileActions } from "./firlaw-file-actions"
import { FirlawFileActions } from "@/firlaw/file-actions/file-actions"

const MAX_DEPTH = 128

Expand Down Expand Up @@ -148,13 +148,13 @@ const FileTreeNode = (
<Dynamic
component={local.as ?? "div"}
classList={{
"w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
"w-full min-w-0 h-8 flex items-center justify-start gap-x-2.5 rounded-lg px-2.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
"bg-surface-base-active": local.node.path === local.active,
...local.classList,
[local.class ?? ""]: !!local.class,
[local.nodeClass ?? ""]: !!local.nodeClass,
}}
style={`padding-left: ${Math.max(0, 8 + local.level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
style={`padding-left: ${Math.max(0, 10 + local.level * 14 - (local.node.type === "file" ? 24 : 4))}px`}
draggable={local.draggable}
onDragStart={(event: DragEvent) => {
if (!local.draggable) return
Expand All @@ -168,7 +168,7 @@ const FileTreeNode = (
{local.children}
<span
classList={{
"flex-1 min-w-0 text-12-medium whitespace-nowrap truncate": true,
"flex-1 min-w-0 text-14-medium whitespace-nowrap truncate": true,
"text-text-weaker": local.node.ignored,
"text-text-weak": !local.node.ignored && !active(),
}}
Expand All @@ -181,7 +181,7 @@ const FileTreeNode = (
if (!value) return null
if (local.node.type === "file") {
return (
<span class="shrink-0 w-4 text-center text-12-medium" style={kindTextColor(value)}>
<span class="shrink-0 w-5 text-center text-14-medium" style={kindTextColor(value)}>
{kindLabel(value)}
</span>
)
Expand Down Expand Up @@ -429,20 +429,20 @@ export default function FileTree(props: {
kinds={kinds()}
marks={marks()}
>
<div class="size-4 flex items-center justify-center text-icon-weak">
<Icon name={expanded() ? "chevron-down" : "chevron-right"} size="small" />
<div class="size-5 flex items-center justify-center text-icon-weak">
<Icon name={expanded() ? "chevron-down" : "chevron-right"} size="medium" />
</div>
</FileTreeNode>,
)}
</Collapsible.Trigger>
<Collapsible.Content class="relative pt-0.5">
<div
classList={{
"absolute top-0 bottom-0 w-px pointer-events-none bg-border-weak-base opacity-0 transition-opacity duration-150 ease-out motion-reduce:transition-none": true,
"absolute top-0 bottom-0 w-px pointer-events-none bg-border-weak-base opacity-40 transition-opacity duration-150 ease-out motion-reduce:transition-none": true,
"group-hover/filetree:opacity-100": expanded() && deep() === level,
"group-hover/filetree:opacity-50": !(expanded() && deep() === level),
"group-hover/filetree:opacity-60": !(expanded() && deep() === level),
}}
style={`left: ${Math.max(0, 8 + level * 12 - 4) + 8}px`}
style={`left: ${Math.max(0, 10 + level * 14 - 4) + 8}px`}
/>
<Show
when={level < MAX_DEPTH && !chain.includes(key(node.path))}
Expand Down Expand Up @@ -483,7 +483,7 @@ export default function FileTree(props: {
type="button"
onClick={() => props.onFileClick?.(node)}
>
<div class="w-4 shrink-0" />
<div class="w-5 shrink-0" />
<Switch>
<Match when={node.ignored}>
<FileIcon
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { applyPath, backPath, forwardPath } from "./titlebar-history"
import { FirlawLogo } from "./firlaw-logo"
import { FirlawLogo } from "@/firlaw/logo/logo"

type TauriDesktopWindow = {
startDragging?: () => Promise<void>
Expand Down
135 changes: 135 additions & 0 deletions packages/app/src/firlaw/styles/overrides.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Firlaw UI overrides — monochrome court-luxury theme.
* Keep upstream JSX files clean; override visuals here.
*/

/* ─── Firlaw Monochrome Palette ───────────────────────────────────────────── */
:root {
/* Bump this when forcing a new asset hash (CDN cache-bust). */
--firlaw-build: 260529;
--firlaw-titlebar-height: 48px;
--firlaw-titlebar-btn-size: 32px;
--firlaw-titlebar-btn-radius: 6px;
--firlaw-font: 'Times New Roman', Times, serif;
}

/* ─── Titlebar ─────────────────────────────────────────────────────────────── */
header[data-tauri-drag-region] {
height: var(--firlaw-titlebar-height) !important;
border-bottom: 1px solid var(--border-weak-base, rgba(0,0,0,.07));
}

/* NOTE: do NOT override upstream .titlebar-icon — upstream uses h-6 (24px) for nav buttons */

/* ─── Session Header (portal-injected right side) ─────────────────────────── */
.firlaw-open-dir-btn {
height: var(--firlaw-titlebar-btn-size) !important;
border-radius: var(--firlaw-titlebar-btn-radius) !important;
}

.firlaw-dir-label {
display: inline;
font-size: 0.875rem;
padding-left: 0.5rem;
}

.firlaw-header-action-btn {
width: auto !important;
height: var(--firlaw-titlebar-btn-size) !important;
border-radius: var(--firlaw-titlebar-btn-radius) !important;
padding-left: 0.75rem !important;
padding-right: 0.75rem !important;
gap: 0.5rem !important;
}

.firlaw-btn-label {
display: inline;
font-size: 0.875rem;
}

.firlaw-reload-skills-btn {
width: var(--firlaw-titlebar-btn-size) !important;
height: var(--firlaw-titlebar-btn-size) !important;
border-radius: var(--firlaw-titlebar-btn-radius) !important;
}

/* ─── Firlaw User Chip (topbar right) ─────────────────────────────────────── */
.firlaw-user-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 3px 10px 3px 4px;
border-radius: 7px;
cursor: pointer;
transition: background .12s;
font-family: var(--firlaw-font);
}
.firlaw-user-chip:hover {
background: var(--surface-base-hover, rgba(0,0,0,.06));
}
.firlaw-user-avatar {
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--text-strong, #161618);
color: var(--background-base, #fff);
display: grid;
place-items: center;
font-weight: 700;
font-size: 11.5px;
font-family: var(--firlaw-font);
}
.firlaw-user-name {
font-size: 14px;
font-weight: 500;
font-family: var(--firlaw-font);
}

/* ─── Firlaw Topbar Button ────────────────────────────────────────────────── */
.firlaw-top-btn {
width: 32px;
height: 32px;
display: grid;
place-items: center;
border-radius: 6px;
border: none;
background: none;
color: var(--icon-weak, #6B6B70);
cursor: pointer;
transition: background .12s, color .12s;
}
.firlaw-top-btn:hover {
background: var(--surface-base-hover, rgba(0,0,0,.06));
color: var(--text-strong, #161618);
}
.firlaw-top-btn svg {
width: 16px;
height: 16px;
}

/* ─── Firlaw Case Toggle Button ───────────────────────────────────────────── */
.firlaw-case-toggle {
display: flex;
align-items: center;
gap: 7px;
height: 32px;
padding: 0 12px 0 10px;
margin-left: 6px;
border: 1px solid var(--border-base, rgba(0,0,0,.12));
border-radius: 7px;
font-size: 13.5px;
font-weight: 500;
font-family: var(--firlaw-font);
color: var(--text-base, #3F3F45);
background: transparent;
cursor: pointer;
transition: background .12s, border-color .12s, color .12s;
}
.firlaw-case-toggle:hover {
background: var(--surface-base-hover, rgba(0,0,0,.06));
color: var(--text-strong, #161618);
}
.firlaw-case-toggle svg {
width: 15px;
height: 15px;
}
106 changes: 106 additions & 0 deletions packages/app/src/firlaw/titlebar/titlebar-extras.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { createSignal, onMount, Show } from "solid-js"
import { Portal } from "solid-js/web"
import { useTheme } from "@opencode-ai/ui/theme/context"
import { Tooltip } from "@opencode-ai/ui/tooltip"

export function FirlawTitlebarExtras() {
const theme = useTheme()
const [mount, setMount] = createSignal<HTMLElement | null>(null)
const [userName, setUserName] = createSignal("")
const [userInitials, setUserInitials] = createSignal("FL")

onMount(() => {
setMount(document.getElementById("opencode-titlebar-right"))
fetchUser()
})

const fetchUser = async () => {
try {
const r = await fetch("/api/v2/auth/get-session", { credentials: "include" })
if (!r.ok) return
const data = await r.json()
const name = data?.user?.name || ""
setUserName(name)
const parts = name.split(" ").filter(Boolean)
if (parts.length >= 2) {
setUserInitials(parts[0][0] + parts[parts.length - 1][0])
} else if (parts.length === 1) {
setUserInitials(parts[0].slice(0, 2).toUpperCase())
}
} catch {}
}

const toggleTheme = () => {
const current = theme.colorScheme()
const next = current === "dark" ? "light" : current === "light" ? "system" : "dark"
theme.setColorScheme(next)
}

const logout = async () => {
try {
await fetch("/api/v2/auth/sign-out", { method: "POST", credentials: "include" })
} catch {
return
}
window.location.href = "/login.html"
}

return (
<Show when={mount()}>
{(el) => (
<Portal mount={el()}>
<div class="flex items-center gap-1" style={{ order: "-1" }}>
<Tooltip placement="bottom" value="Thông báo">
<button class="firlaw-top-btn" aria-label="Thông báo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
<path d="M10 21a2 2 0 0 0 4 0" />
</svg>
</button>
</Tooltip>

<Tooltip placement="bottom" value={theme.colorScheme() === "dark" ? "Chế độ sáng" : theme.colorScheme() === "light" ? "Theo hệ thống" : "Chế độ tối"}>
<button class="firlaw-top-btn" onClick={toggleTheme} aria-label="Đổi giao diện">
<Show
when={theme.colorScheme() === "dark"}
fallback={
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" /><line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" /><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" /><line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" /><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
</Show>
</button>
</Tooltip>

<Tooltip placement="bottom" value="Trợ giúp">
<button class="firlaw-top-btn" aria-label="Trợ giúp">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
</button>
</Tooltip>

<Show when={userName()}>
<Tooltip placement="bottom" value="Đăng xuất">
<button class="firlaw-user-chip" onClick={logout} aria-label={`${userName()} — Đăng xuất`}>
<div class="firlaw-user-avatar">{userInitials()}</div>
<span class="firlaw-user-name">{userName()}</span>
</button>
</Tooltip>
</Show>
</div>
</Portal>
)}
</Show>
)
}
2 changes: 1 addition & 1 deletion packages/app/src/index.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@import "@opencode-ai/ui/styles/tailwind";
@import "./styles/firlaw-overrides.css";
@import "./firlaw/styles/overrides.css";

@font-face {
font-family: "JetBrainsMono Nerd Font Mono";
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { useCommand, type CommandOption } from "@/context/command"
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
import { DebugBar } from "@/components/debug-bar"
import { Titlebar } from "@/components/titlebar"
import { FirlawTitlebarExtras } from "@/firlaw/titlebar/titlebar-extras"
import { useServer } from "@/context/server"
import { useLanguage, type Locale } from "@/context/language"
import { pathKey } from "@/utils/path-key"
Expand Down Expand Up @@ -2382,6 +2383,7 @@ export default function Layout(props: ParentProps) {
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
{autoselecting() ?? ""}
<Titlebar />
<FirlawTitlebarExtras />
<div class="flex-1 min-h-0 min-w-0 flex">
<div class="flex-1 min-h-0 relative">
<div class="size-full relative overflow-x-hidden">
Expand Down
10 changes: 7 additions & 3 deletions packages/app/src/pages/session/session-side-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { useDialog } from "@opencode-ai/ui/context/dialog"

import FileTree from "@/components/file-tree"
import { FirlawSidePanelActions } from "./firlaw-side-panel-actions"
import { FirlawSidePanelActions } from "@/firlaw/side-panel/side-panel-actions"
import { SessionContextUsage } from "@/components/session-context-usage"
import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
import { useCommand } from "@/context/command"
Expand Down Expand Up @@ -385,8 +385,12 @@ export function SessionSidePanel(props: {
class="h-full"
data-scope="filetree"
>
<div class="flex items-center gap-1">
<Tabs.List class="flex-1">
<div class="flex flex-col gap-2 px-3 pt-3 pb-2">
<div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-[#C9A961]" />
<span class="text-13-medium text-text-strong">Thư mục dự án</span>
</div>
<Tabs.List class="w-full">
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
{props.reviewCount()}{" "}
{language.t(
Expand Down
Loading
Loading