Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
- Runtime: Node 24 through `mise exec node@24 --`; pnpm 11.2.1 through Corepack.
- Install with `pnpm install`, create the documented `.env` symlinks, then run `pnpm db:migrate`.
- Start local development with `pnpm dev:start`. Use `pnpm dev:start -d` for detached mode and `pnpm dev:stop` to stop only that workspace.
- Shared local infrastructure is machine-level: one Meilisearch at `http://localhost:7700` and one Chrome/CDP at `http://localhost:9222`. Manage it explicitly with `pnpm dev:infra:up`, `pnpm dev:infra:status`, and `pnpm dev:infra:down`.
- Shared local infrastructure is machine-level: one Meilisearch at `http://localhost:7700` and one Chrome/CDP at `http://localhost:9250`. Manage it explicitly with `pnpm dev:infra:up`, `pnpm dev:infra:status`, and `pnpm dev:infra:down`.
- Parallel worktrees keep separate SQLite/assets data and unique web ports. `scripts/setup-worktree.sh` assigns each worktree a unique `MEILI_INDEX_PREFIX`; both `bookmarks` and `bookmarks_vectors` use that namespace on the shared Meilisearch server.
- `pnpm dev:start` defaults the main workspace namespace to `main_`. An unset `MEILI_INDEX_PREFIX` is a compatibility fallback for the original `bookmarks` and `bookmarks_vectors` names; manual `web` or `workers` starts outside `pnpm dev:start` must set an explicit unique prefix.
- Run focused checks before broad checks when practical. Standard checks are `pnpm format:fix`, `pnpm lint`, `pnpm typecheck`, and `pnpm test`.
Expand Down Expand Up @@ -126,7 +126,7 @@ Useful variants:
Local-dev ownership model:
- `web` + `workers` run natively per workspace
- one machine-level Meilisearch container is shared at `http://localhost:7700`
- one machine-level Chrome container is shared at `http://localhost:9222`
- one machine-level Chrome container is shared at `http://localhost:9250`
- `pnpm dev:start` automatically ensures those shared containers exist
- `pnpm dev:stop` never stops shared infrastructure because other worktrees may still use it
- the shared Chrome image is `ghcr.io/karakeep-app/karakeep-chrome:release`
Expand All @@ -152,7 +152,7 @@ pnpm workers

Notes:
- Meilisearch and headless Chrome are optional for booting the app, but required for full search/crawling behavior.
- shared infra binds only to localhost; if ports `7700` or `9222` are occupied by something else, the helper fails rather than silently reusing an unknown service
- shared infra binds only to localhost; if ports `7700` or `9250` are occupied by something else, the helper fails rather than silently reusing an unknown service
- If `next dev` crashes with a stale Turbopack/instrumentation issue, clear `apps/web/.next`.

### Pull prod state to local dev
Expand Down
2 changes: 1 addition & 1 deletion apps/browser-extension/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Karakeep</title>
<title>Marka</title>
</head>
<body>
<div id="root"></div>
Expand Down
6 changes: 3 additions & 3 deletions apps/browser-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Karakeep",
"description": "An extension to bookmark links to karakeep.app",
"name": "Marka",
"description": "An extension to bookmark links to your Marka server",
"version": "1.2.11",
"icons": {
"16": "public/logo-16.png",
Expand Down Expand Up @@ -65,7 +65,7 @@
"suggested_key": {
"default": "Ctrl+Shift+E"
},
"description": "Send the current page URL to Karakeep."
"description": "Send the current page URL to Marka."
}
}
}
4 changes: 2 additions & 2 deletions apps/browser-extension/src/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import logoImg from "../public/logo-full.png";
export default function Logo() {
return (
<span className="flex items-center justify-center">
<img src={logoImg} alt="karakeep logo" className="h-14 dark:hidden" />
<img src={logoImg} alt="Marka logo" className="h-14 dark:hidden" />
<img
src={logoImgWhite}
alt="karakeep logo"
alt="Marka logo"
className="hidden h-14 dark:block"
/>
</span>
Expand Down
6 changes: 3 additions & 3 deletions apps/browser-extension/src/background/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,20 @@ function registerContextMenus(settings: Settings) {
removeContextMenus();
chrome.contextMenus.create({
id: OPEN_KARAKEEP_ID,
title: "Open Karakeep",
title: "Open Marka",
contexts: ["action"],
});

chrome.contextMenus.create({
id: ADD_LINK_TO_KARAKEEP_ID,
title: "Add to Karakeep",
title: "Add to Marka",
contexts: ["link", "page", "selection", "image"],
});

if (settings?.showCountBadge) {
chrome.contextMenus.create({
id: VIEW_PAGE_IN_KARAKEEP,
title: "View this page in Karakeep",
title: "View this page in Marka",
contexts: ["action", "page"],
});
if (settings?.useBadgeCache) {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "@karakeep/cli",
"version": "0.32.0",
"description": "Command Line Interface (CLI) for Karakeep",
"description": "Command Line Interface (CLI) for Marka",
"license": "GNU Affero General Public License version 3",
"type": "module",
"keywords": [
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ authCmd
opts.serverAddr ??
(await promptForValue(
rl,
"Karakeep server address",
"Marka server address",
existingAuth.serverAddr ?? DEFAULT_SERVER_ADDR,
));
const apiKey =
opts.apiKey ??
(await promptForValue(rl, "Karakeep API key", existingAuth.apiKey));
(await promptForValue(rl, "Marka API key", existingAuth.apiKey));
rl.close();

if (!serverAddr || !apiKey) {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/dump.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const dumpCmd = new Command()

try {
line("");
line(`${chalk.bold("Karakeep Dump")}`);
line(`${chalk.bold("Marka Dump")}`);
line(`${chalk.gray("Server:")} ${globals.serverAddr}`);
line(`${chalk.gray("Output:")} ${outFile}`);
line("");
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const migrateCmd = new Command()

try {
line("");
line(`${chalk.bold("Karakeep Migration")}`);
line(`${chalk.bold("Marka Migration")}`);
line(`${chalk.gray("From:")} ${globals.serverAddr}`);
line(`${chalk.gray("To: ")} ${opts.destServer}`);
line("");
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/wipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const wipeCmd = new Command()

try {
line("");
line(`${chalk.bold("Karakeep Wipe")}`);
line(`${chalk.bold("Marka Wipe")}`);
line(`${chalk.gray("Server:")} ${globals.serverAddr}`);
line("");

Expand Down
4 changes: 2 additions & 2 deletions apps/mcp/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Karakeep MCP server
# Marka MCP server

This package contains the MCP server for Karakeep.
This package contains the MCP server for Marka.

It exposes bookmark/list/tag operations for external MCP-compatible tools and agents.

Expand Down
2 changes: 1 addition & 1 deletion apps/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "@karakeep/mcp",
"version": "0.32.0",
"description": "MCP server for Karakeep",
"description": "MCP server for Marka",
"license": "GNU Affero General Public License version 3",
"type": "module",
"keywords": [
Expand Down
2 changes: 1 addition & 1 deletion apps/mcp/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const karakeepClient = createKarakeepClient({
});

export const mcpServer = new McpServer({
name: "Karakeep",
name: "Marka",
version: "0.23.0",
});

Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ export default {
expo: {
...(IS_DEV
? {
name: "Karakeep (Dev)",
name: "Marka (Dev)",
scheme: "karakeep-dev",
}
: {
name: "Karakeep",
name: "Marka",
scheme: "karakeep",
}),
slug: "hoarder",
Expand Down
10 changes: 1 addition & 9 deletions apps/mobile/app/dashboard/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,7 @@ export default function TabLayout() {
<Label>Tags</Label>
</NativeTabs.Trigger>

<NativeTabs.Trigger name="(highlights)">
<Icon
sf="highlighter"
androidSrc={
<VectorIcon family={MaterialCommunityIcons} name="marker" />
}
/>
<Label>Highlights</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(highlights)" hidden />

<NativeTabs.Trigger
name="(search)"
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/app/server-address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default function ServerAddress() {
</Text>
<View className="w-full gap-3 rounded-lg bg-card px-4 py-4">
<Text className="text-sm text-muted-foreground">
Enter the URL of your Karakeep server
Enter the URL of your Marka server
</Text>
<Input
placeholder="https://cloud.karakeep.app"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `@karakeep/web`

This is the main Next.js web application for Karakeep.
This is the main Next.js web application for Marka.

## What lives here

Expand Down
8 changes: 8 additions & 0 deletions apps/web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,14 @@
}
}

@media (max-width: 639px) {
/* Leave a quiet strip below bottom sheets for the phone gesture handle. */
.dialog-vv-bottom-safe-area {
background-color: var(--card) !important;
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 1.5rem) !important;
}
}

/* ============================================================
Mobile / touch / PWA ergonomics. Deliberately UNLAYERED: cascade layers
rank unlayered rules above any @layer, so the 16px form-control size below
Expand Down
32 changes: 30 additions & 2 deletions apps/web/components/dashboard/header/Header.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// @vitest-environment jsdom

import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

vi.mock("next/link", () => ({
default: ({ children, href, ...props }: React.ComponentProps<"a">) => (
Expand Down Expand Up @@ -35,14 +35,42 @@ vi.mock("@/server/auth", () => ({
import Header from "./Header";

describe("Header", () => {
afterEach(() => {
cleanup();
});

it("uses a left-aligned Marka wordmark in the original reserved logo slot", async () => {
render(await Header());

const header = screen.getByRole("banner");
const link = screen.getByRole("link", { name: "Marka" });
const search = screen.getByRole("textbox", { name: "Search" });
expect(link.getAttribute("href")).toBe("/dashboard/bookmarks");
expect(link.classList.contains("justify-start")).toBe(true);
expect(link.classList.contains("w-56")).toBe(true);
expect(link.classList.contains("pl-2")).toBe(true);
expect(screen.getByAltText("Marka").getAttribute("height")).toBe("30");
expect(header.firstElementChild?.className).toContain(
"lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)_minmax(0,1fr)]",
);
expect(search.parentElement?.className).toContain("lg:col-start-2");
});

it("groups the status rectangle and profile avatar with the avatar on top", async () => {
render(await Header());

const profile = screen.getByRole("button", { name: "Profile" });
const avatarLayer = profile.parentElement;
const controls = avatarLayer?.parentElement;

expect(avatarLayer?.className).toContain("absolute");
expect(avatarLayer?.className).toContain("right-0");
expect(avatarLayer?.className).toContain("top-1/2");
expect(avatarLayer?.className).toContain("-translate-y-1/2");
expect(avatarLayer?.className).toContain("size-10");
expect(avatarLayer?.className).toContain("z-10");
expect(controls?.className).toContain("relative");
expect(controls?.className).toContain("h-10");
expect(controls?.className).toContain("pr-5");
});
});
10 changes: 6 additions & 4 deletions apps/web/components/dashboard/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default async function Header() {

return (
<header className="bg-sidebar/95 sticky left-0 right-0 top-0 z-50">
<div className="flex h-16 items-center gap-3 overflow-x-auto overflow-y-hidden px-3 sm:px-4">
<div className="flex h-16 items-center gap-3 overflow-x-auto overflow-y-hidden px-3 sm:px-4 lg:grid lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)_minmax(0,1fr)]">
<div className="hidden shrink-0 items-center sm:flex">
<Link
href={"/dashboard/bookmarks"}
Expand All @@ -24,12 +24,14 @@ export default async function Header() {
<MarkaLogo height={30} />
</Link>
</div>
<div className="min-w-0 flex-1">
<div className="min-w-0 flex-1 lg:col-start-2 lg:row-start-1">
<SearchInput className="w-full" />
</div>
<div className="ml-3 flex shrink-0 items-center gap-2 sm:ml-4">
<div className="relative ml-3 flex h-10 shrink-0 items-center pr-5 sm:ml-4 lg:col-start-3 lg:row-start-1 lg:justify-self-end">
<ProcessingStatusIndicator />
<ProfileOptions />
<div className="absolute right-0 top-1/2 z-10 size-10 -translate-y-1/2">
<ProfileOptions />
</div>
</div>
</div>
</header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ export default function ProcessingStatusIndicator() {
<Button
type="button"
variant="ghost"
className="shadow-xs ease-(--ease-out) h-10 gap-1.5 rounded-xl border border-border/70 bg-background px-2.5 text-foreground transition-[background-color,border-color,box-shadow] duration-150 hover:bg-accent/70"
className="shadow-xs ease-(--ease-out) h-10 min-w-16 justify-start gap-1.5 rounded-l-xl rounded-r-none border border-border/70 bg-background px-2.5 text-foreground transition-[background-color,border-color,box-shadow] duration-150 hover:bg-accent/70"
aria-label={buttonLabel}
>
<Icon
Expand Down
4 changes: 2 additions & 2 deletions apps/web/components/dashboard/header/ProfileOptions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ vi.mock("@/lib/i18n/client", () => ({
apps_extensions: "Apps & extensions",
coming_soon: "Coming soon",
documentation: "Documentation",
update_now: "Update now",
};
return profileTranslations[key] ?? key;
}
Expand Down Expand Up @@ -142,11 +143,10 @@ describe("ProfileOptions", () => {
expect(disabledItems).toHaveLength(2);
});

it("shows the shared build footer only in the mobile profile layout", () => {
it("shows the shared build footer in the profile menu", () => {
render(<ProfileOptions />);

const version = screen.getByTestId("sidebar-version");
expect(version.getAttribute("data-placement")).toBe("profile");
expect(version.parentElement?.className).toContain("sm:hidden");
});
});
9 changes: 3 additions & 6 deletions apps/web/components/dashboard/header/ProfileOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ export default function SidebarProfileOptions() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="shadow-xs ease-(--ease-out) aspect-square size-10 rounded-full border border-border/70 bg-background p-0 text-foreground transition-[background-color,border-color,box-shadow] duration-150 hover:bg-accent/70"
className="shadow-xs ease-(--ease-out) flex size-10 shrink-0 rounded-full border border-border/70 bg-background p-0 text-foreground transition-[background-color,border-color,box-shadow] duration-150 hover:bg-accent/70"
variant="ghost"
>
<UserAvatar
image={avatarUrl}
name={session.user.name}
className="h-full w-full rounded-full"
className="size-full rounded-full border border-border/70"
/>
</Button>
</DropdownMenuTrigger>
Expand Down Expand Up @@ -160,14 +160,11 @@ export default function SidebarProfileOptions() {
comingSoon={tProfile("coming_soon")}
/>
<Separator className="my-2" />
<div className="sm:hidden">
<SidebarVersion placement="profile" />
</div>
<Separator className="my-2 sm:hidden" />
<DropdownMenuItem onClick={() => router.push("/logout")}>
<LogOut className="mr-2 size-4" />
<span>{t("actions.sign_out")}</span>
</DropdownMenuItem>
<SidebarVersion placement="profile" />
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
Loading
Loading