Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore",
"less.lint.unknownAtRules": "ignore"
}
9 changes: 7 additions & 2 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { copyFileSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs";
import { dirname, join, relative, resolve } from "path";
import { defineConfig, type HeadConfig } from "vitepress";
import { extendConfig } from "@voidzero-dev/vitepress-theme/config";
import { tabsMarkdownPlugin } from "vitepress-plugin-tabs";

function loadEnvVar(key: string): string | undefined {
Expand Down Expand Up @@ -45,7 +46,7 @@ const searchConfig =
}
: { provider: "local" as const };

export default defineConfig({
const config = defineConfig({
title: "Plane",
description: "Modern project management software",
cleanUrls: true,
Expand Down Expand Up @@ -195,11 +196,12 @@ export default defineConfig({
],

themeConfig: {
variant: "voidzero",
logo: {
light: "https://media.docs.plane.so/logo/new-logo-white.png",
dark: "https://media.docs.plane.so/logo/new-logo-dark.png",
},
siteTitle: "",
siteTitle: "Plane",

Comment on lines 225 to 232
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@voidzero-dev/vitepress-theme themeConfig.variant supported values list

💡 Result:

The supported values for themeConfig.variant in @voidzero-dev/vitepress-theme are strings corresponding to VoidZero projects, such as "viteplus", "vite", "vitest". Additional likely values based on project context include "rolldown" and "oxc", as the theme is designed for Vite+, Vite, Vitest, Rolldown, and Oxc. The variant controls branding colors and styling via CSS selectors like :root[data-variant="vite"]. Users import specific variants like '@voidzero-dev/vitepress-theme/src/vite' in the theme entry.

Citations:


Change variant: "voidzero" to a supported value.

The @voidzero-dev/vitepress-theme package only supports variants: "vite", "viteplus", "vitest", "rolldown", and "oxc". The variant "voidzero" is not recognized and will cause incorrect theme rendering or build failures. Use one of the supported variants that matches your target project.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/.vitepress/config.ts` around lines 225 - 232, The themeConfig currently
sets variant: "voidzero", which is unsupported by `@voidzero-dev/vitepress-theme`;
update the variant property inside themeConfig to one of the supported values
("vite", "viteplus", "vitest", "rolldown", or "oxc") that best matches your
project (e.g., change variant in themeConfig from "voidzero" to "vite" or
another supported option) so the theme renders and builds correctly.

outline: {
level: [2, 3],
Expand All @@ -224,6 +226,7 @@ export default defineConfig({
{
text: "Sign in",
link: "https://app.plane.so/sign-in",
noIcon: true,
},
],

Expand Down Expand Up @@ -682,3 +685,5 @@ export default defineConfig({
},
},
});

export default extendConfig(config);
16 changes: 16 additions & 0 deletions docs/.vitepress/theme/Layout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script setup lang="ts">
import { computed, useSlots } from "vue";
import VoidZeroTheme from "@voidzero-dev/vitepress-theme";

const BaseLayout = VoidZeroTheme.Layout;
const slots = useSlots();
const forwardSlotNames = computed(() => Object.keys(slots) as string[]);
</script>

<template>
<BaseLayout>
<template v-for="name in forwardSlotNames" :key="name" #[name]="data">
<slot :name="name" v-bind="data || {}" />
</template>
</BaseLayout>
</template>
27 changes: 21 additions & 6 deletions docs/.vitepress/theme/components/Card.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ import * as LucideIcons from "lucide-vue-next";
const props = defineProps({
title: String,
icon: String,
/** Doc path or full URL */
href: String,
/** Alias for `href` (common in markdown) */
link: String,
/** When set, used instead of the default slot for body text */
description: String,
/** Bottom link label (shown with →). Home feature cards only. */
cta: String,
});

const resolvedHref = computed(() => props.link || props.href || "#");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Custom SVG icons for brands
const customSvgIcons = {
asana: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18.559 13.605a5.158 5.158 0 1 0 0 10.317 5.158 5.158 0 0 0 0-10.317Zm-13.401.001a5.158 5.158 0 1 0 0 10.315 5.158 5.158 0 0 0 0-10.315Zm11.858-6.448a5.158 5.158 0 1 1-10.316 0 5.158 5.158 0 0 1 10.316 0Z" fill="#F06A6A"/></svg>`,
Expand Down Expand Up @@ -44,18 +53,24 @@ const IconComponent = computed(() => {
</script>

<template>
<a :href="href" class="card-link">
<div v-if="icon" class="card-icon">
<!-- Render custom SVG if available -->
<a
:href="resolvedHref"
class="card-link"
:class="{ 'card-link--with-cta': cta }"
>
Comment on lines +63 to +68
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

External links open in same tab without rel="noopener".

The homepage uses link="https://developers.plane.so/self-hosting/overview" which is external. The rendered <a> lacks target and rel attributes, so external destinations open in the same tab (losing docs context) and miss rel="noopener noreferrer" hardening.

♻️ Proposed refinement
+<script setup>
+// ...existing code...
+const isExternal = computed(() => /^https?:\/\//i.test(resolvedHref.value));
+</script>

 <template>
   <component
     :is="hasDestination ? 'a' : 'div'"
     :href="hasDestination ? resolvedHref : undefined"
+    :target="hasDestination && isExternal ? '_blank' : undefined"
+    :rel="hasDestination && isExternal ? 'noopener noreferrer' : undefined"
     class="card-link"
     :class="{ 'card-link--with-cta': cta }"
   >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/.vitepress/theme/components/Card.vue` around lines 63 - 68, The anchor
rendered by Card.vue uses hasDestination and resolvedHref but doesn't set
target/rel for external URLs; update the component rendering logic (using
hasDestination, resolvedHref and cta) to detect external links (e.g.,
resolvedHref startsWith('http') or origin !== current origin) and, only for
external destinations, set target="_blank" and rel="noopener noreferrer" on the
<a> element while leaving internal links unchanged.

<div v-if="icon" class="card-icon" aria-hidden="true">
<div v-if="customSvgIcons[icon]" v-html="customSvgIcons[icon]"></div>
<!-- Otherwise render Lucide icon -->
<component v-else :is="IconComponent" :size="24" />
<component v-else :is="IconComponent" :size="24" stroke-width="1.5" />
</div>
<h3 class="card-title">
{{ title }}
</h3>
<p class="card-description">
<p v-if="description" class="card-description">
{{ description }}
</p>
<p v-else class="card-description">
<slot />
</p>
<span v-if="cta" class="card-cta">{{ cta }} →</span>
</a>
</template>
17 changes: 13 additions & 4 deletions docs/.vitepress/theme/components/CardGroup.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
<script setup>
defineProps({
import { computed } from "vue";

const props = defineProps({
cols: {
type: [Number, String],
default: 2,
},
/** Alias for `cols` (e.g. `<CardGroup columns="3">`) */
columns: {
type: [Number, String],
default: undefined,
},
});

const columnCount = computed(() => props.columns ?? props.cols);
</script>

<template>
<div
class="card-group"
:class="{
'card-group-2': cols == 2,
'card-group-3': cols == 3,
'card-group-4': cols == 4,
'card-group-2': columnCount == 2,
'card-group-3': columnCount == 3,
'card-group-4': columnCount == 4,
}"
>
<slot />
Expand Down
72 changes: 65 additions & 7 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import DefaultTheme from "vitepress/theme";
import type { Theme } from "vitepress";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
import type { ThemeContext } from "@voidzero-dev/vitepress-theme";
import VoidZeroTheme from "@voidzero-dev/vitepress-theme";
import { themeContextKey } from "@voidzero-dev/vitepress-theme";
import { onMounted, watch, nextTick } from "vue";
import { useRoute } from "vitepress";
import { enhanceAppWithTabs } from "vitepress-plugin-tabs/client";
import mediumZoom from "medium-zoom";
import Card from "./components/Card.vue";
import CardGroup from "./components/CardGroup.vue";
import Tags from "./components/Tags.vue";
import Layout from "./Layout.vue";
import "./style.css";

/**
* OSSHeader (used on doc pages) injects this context for the bar logo — *not* `themeConfig.logo`.
* The `viteplus` entry in the package overwrites it with "Vite+" assets; we use the base
* `VoidZeroTheme` and provide Plane branding here.
*/
const planeThemeContext: ThemeContext = {
/* OSSHeader renders logoDark in light mode and logoLight in dark mode. */
logoDark: "https://media.docs.plane.so/logo/new-logo-white.png",
logoLight: "https://media.docs.plane.so/logo/new-logo-dark.png",
logoAlt: "Plane",
footerBg: "https://media.docs.plane.so/logo/og-docs.webp",
monoIcon: "https://media.docs.plane.so/logo/favicon-32x32.png",
};

/**
* Handles tab activation based on URL hash
*/
Expand Down Expand Up @@ -73,13 +90,52 @@ function updateHashOnTabClick(event: Event) {
}
}

/**
* Move "Sign in" CTA to the right utility area (between search and theme toggle).
* The upstream OSSHeader renders nav links on the left; we remap only this CTA.
*/
function moveSignInToUtilityArea() {
if (typeof document === "undefined") return;

const signInLink = document.querySelector(
'.docs-layout header .VPNavBarMenu a.VPLink[href*="sign-in"]',
) as HTMLAnchorElement | null;
if (!signInLink) return;

const appearanceToggle = document.querySelector(
".docs-layout header .VPNavBarAppearance",
) as HTMLElement | null;

// Desktop (xl+): insert before theme toggle inside the utilities row.
if (
appearanceToggle?.parentElement &&
signInLink.parentElement !== appearanceToggle.parentElement
) {
signInLink.classList.add("sign-in-relocated");
appearanceToggle.parentElement.insertBefore(signInLink, appearanceToggle);
return;
}

// Tablet fallback (lg-xl): keep it in right controls row before the extra menu.
const extraMenu = document.querySelector(
".docs-layout header .VPNavBarExtra",
) as HTMLElement | null;
if (extraMenu?.parentElement && signInLink.parentElement !== extraMenu.parentElement) {
signInLink.classList.add("sign-in-relocated");
extraMenu.parentElement.insertBefore(signInLink, extraMenu);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

export default {
extends: DefaultTheme,
enhanceApp({ app }) {
enhanceAppWithTabs(app);
app.component("Card", Card);
app.component("CardGroup", CardGroup);
app.component("Tags", Tags);
extends: VoidZeroTheme,
Layout,
enhanceApp(ctx) {
ctx.app.provide(themeContextKey, planeThemeContext);
VoidZeroTheme.enhanceApp(ctx);
enhanceAppWithTabs(ctx.app);
ctx.app.component("Card", Card);
ctx.app.component("CardGroup", CardGroup);
ctx.app.component("Tags", Tags);
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
setup() {
if (typeof window === "undefined") return;
Expand All @@ -95,6 +151,7 @@ export default {
setTimeout(() => {
handleTabHash();
setupTabHashUpdates();
moveSignInToUtilityArea();
}, 100);

// Listen for hash changes
Expand All @@ -112,6 +169,7 @@ export default {
zoom.attach(":not(a) > img:not(.VPImage)");
handleTabHash();
setupTabHashUpdates();
moveSignInToUtilityArea();
});
},
);
Expand Down
Loading
Loading