This commit is contained in:
Fedor Katurov 2022-11-02 11:58:45 +06:00
commit 5104c2518b
34 changed files with 6844 additions and 0 deletions

11
.dockerignore Normal file
View file

@ -0,0 +1,11 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
.idea
.vscode
content/.obsidian
.DS_Store

28
.drone.docker.yml Normal file
View file

@ -0,0 +1,28 @@
kind: pipeline
name: build
type: docker
platform:
os: linux
arch: amd64
steps:
- name: build-master
image: plugins/docker
when:
branch:
- master
settings:
dockerfile: docker/Dockerfile
tag:
- ${DRONE_BRANCH}
custom_labels:
- "commit=${DRONE_COMMIT_SHA}"
username:
from_secret: global_docker_login
password:
from_secret: global_docker_password
registry:
from_secret: global_docker_registry
repo:
from_secret: docker_repo

29
.drone.gh-pages.yml Normal file
View file

@ -0,0 +1,29 @@
kind: pipeline
name: build
type: docker
platform:
os: linux
arch: amd64
steps:
- name: build
image: node:16
commands:
- yarn
- yarn generate
- rm -rf ./docs
- mv ./.output/public ./docs
- touch ./docs/.nojekyll
- name: publish
image: plugins/gh-pages
settings:
target_branch: gh-pages
ssh_key:
from_secret: global_ssh_key
username:
from_secret: github_username
password:
from_secret: global_github_token
ssh_key:
from_secret: global_ssh_key

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
dist
.idea
.vscode
content/.obsidian
.DS_Store
.obsidian

0
.nojekyll Normal file
View file

35
README.md Normal file
View file

@ -0,0 +1,35 @@
# Self-hosted Obsidian Vault
Use [Obsidian](https://obsidian.md) for content editing at `./content` folder. Made with
[NuxtJS](https://v3.nuxtjs.org) and [Nuxt Content Plugin](https://content.nuxtjs.org/).
## Running
```shell
yarn
yarn dev
```
## Publishing
```shell
yarn generate
cp -a ./.outputs/public ./somewhere
```
- Dockerfile included in `./docker`
- Sample `drone-ci` configurations for gh-pages (`./.drone.gh-pages.yml`) and docker registry
(`./.drone.docker.yml`).
## Supported Obsidian features
- WikiLinks (should be set up to relative in order to work)
- Highlight
- Code blocks
- Nested pages
## Other feature
- Adaptive layout
- SEO Optimized
- Day / Night theme switching

27
assets/css/_mixins.scss Normal file
View file

@ -0,0 +1,27 @@
@import "./variables";
@mixin phone {
@media (max-width: $size-phone) {
@content;
}
}
@mixin tablet {
@media (max-width: $size-tablet) {
@content;
}
}
@mixin desktop {
@media (max-width: $size-desktop) {
@content;
}
}
@mixin color-per-child($colors) {
@each $color in $colors {
&:nth-child(#{index(($colors), ($color))}) {
color: $color;
}
}
}

View file

@ -0,0 +1,10 @@
.page-enter-active,
.page-leave-active {
transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
transform: translate(0, 50px);
}

View file

@ -0,0 +1,3 @@
$size-phone: 560px;
$size-tablet: 768px;
$size-desktop: 1024px;

206
assets/css/main.scss Normal file
View file

@ -0,0 +1,206 @@
@import url("https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@600&family=Roboto:wght@400;700&display=swap");
@import "./variables";
@import "./transitions.scss";
body,
html {
font-family: var(--family-roboto);
background: var(--color-background);
color: var(--color-text);
padding: 0;
margin: 0;
}
* {
box-sizing: border-box;
transition: color 250ms;
}
a {
color: var(--color-link);
h1 > &,
h2 > &,
h3 > &,
h4 > &,
h5 > & {
text-decoration: none;
color: var(--color-header);
}
}
pre {
background-color: var(--color-code-background);
padding: 10px;
border-radius: 10px;
overflow: scroll;
width: 100%;
line-height: 1.5em;
}
p > code {
background-color: var(--color-code-background);
color: var(--color-code-inline);
padding: 0 5px;
border-radius: 4px;
}
h1,
h2,
h3,
h4,
h5 {
font-family: var(--family-roboto-slab);
color: var(--color-header);
font-weight: 700;
}
h1 {
color: var(--color-heading-primary);
font-size: 2.6rem;
margin-bottom: 1.5rem;
&:not(:first-child) {
margin-top: 3rem;
}
}
h2 {
color: var(--color-heading-secondary);
&:not(:first-child) {
margin-top: 2rem;
}
}
h3,
h4,
h5 {
color: var(--color-heading-tertiary);
}
p,
li {
line-height: 1.45em;
}
li {
&:not(:last-child) {
margin-bottom: 0.25em;
}
}
button {
background: none;
border: none;
padding: 0;
&:focus {
border: none;
}
}
table {
border-collapse: collapse;
border: 2px solid var(--color-line);
td,
th {
border: 1px solid var(--color-line);
padding: 5px 10px;
text-align: left;
}
thead {
background: var(--color-table-head);
border-bottom: 2px solid var(--color-line);
}
}
blockquote {
border-left: 3px solid var(--color-primary);
color: var(--color-text);
padding: 0 20px;
ul,
ol {
padding-left: 15px;
}
}
.highlight {
background-color: var(--color-highlight-background);
color: var(--color-highlight-color);
padding: 0 1px;
border-radius: 3px;
}
:root {
// fonts
--family-roboto-slab: "Roboto Slab", "Segoe UI", Tahoma, Geneva, Verdana,
sans-serif;
--family-roboto: "Roboto", "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
// breakpoints
--size-phone: $phone;
--size-tablet: $size-tablet;
--size-desktop: $size-desktop;
}
:root.dark {
// palette
--color-primary: #e84a72;
--color-background: #16161c;
--color-menu-background: #1a1c23;
--color-line: #2d2f36;
--color-menu-overlay-background: #{transparentize(#16161c, 0.1)};
--color-code-background: #1a1c23;
--color-code-language-background: #{lighten(#1a1c23, 4%)};
--color-code-language-name: #1eaeae;
--color-text: #fdf0ed;
--color-text-secondary: #{mix(#ffffff, #1a1c23, 60%)};
--color-link: #e84a72;
--color-code-inline: #1eb980;
--color-heading-primary: white;
--color-heading-secondary: #f9cbbe;
--color-heading-tertiary: #f9cec3;
--color-menu-title: #fadad1;
--color-menu-link: #fab28e;
--color-menu-link-active: #e84a72;
--color-menu-line: #2e303e;
--color-table-head: #{mix(#e84a72, #1a1c23, 10%)};
--color-rating-1: #ded187;
--color-rating-2: #dbde87;
--color-rating-3: #bade87;
--color-rating-4: #9cde87;
--color-rating-5: #87deaa;
--color-highlight-color: var(--color-text);
--color-highlight-background: #254e50;
}
:root.light {
$pinky: #{mix(#fadad1, #fce9e4, 50%)};
--color-primary: #e84a72;
--color-background: #fce9e4;
--color-menu-background: #{$pinky};
--color-line: #{$pinky};
--color-menu-overlay-background: #{transparentize(#16161c, 0.1)};
--color-code-background: #{$pinky};
--color-code-language-background: #{lighten(#1a1c23, 4%)};
--color-code-language-name: #1eaeae;
--color-text: #5a5d68;
--color-text-secondary: #{mix(#ffffff, #5a5d68, 20%)};
--color-link: #e84a72;
--color-code-inline: #8931b9;
--color-heading-primary: #4c5161;
--color-heading-secondary: #{mix(#f9cbbe, #1eaeae, 35%)};
--color-heading-tertiary: #{mix(#f9cbbe, #1eaeae, 35%)};
--color-menu-title: #{mix(#f9cbbe, #1eaeae, 35%)};
--color-menu-link: #{mix(#f9cbbe, #e84a72, 20%)};
--color-menu-link-active: #e84a72;
--color-menu-line: #f9cbbe;
--color-table-head: #{mix(#e84a72, #fadad1, 10%)};
--color-highlight-color: var(--color-text);
--color-highlight-background: #fab795;
}

View file

@ -0,0 +1,35 @@
<script lang="ts" setup>
interface Props {
href?: string;
blank?: boolean;
}
withDefaults(defineProps<Props>(), {
href: "",
blank: false,
});
const isInternalLink = (link: string) => !link.match(/^\w+\:\/\//);
const transformInternalLinks = (href: string) => {
if (!isInternalLink(href)) {
return href;
}
return href
.toLowerCase()
.replaceAll("%20", " ")
.replace(/\d+/g, "")
.trim()
.replaceAll(" ", "-");
};
</script>
<template>
<NuxtLink
:href="transformInternalLinks(href)"
:target="isInternalLink(href) ? '' : '_blank'"
>
<slot
/></NuxtLink>
</template>

View file

@ -0,0 +1,81 @@
<script lang="ts" setup>
interface Props {
code?: string;
language?: string | null;
filename?: string | null;
highlights?: number[];
}
const props = withDefaults(defineProps<Props>(), {
language: null,
filename: null,
highlights: () => [],
});
const copy = () => {
navigator.clipboard.writeText(props.code);
};
</script>
<template>
<div :class="$style.wrapper">
<button :class="$style.language" @click="copy">
<span :class="$style.icon">
<UiIconCopy width="12" height="12" fill="currentColor" />
</span>
<span v-if="language">{{ language }}</span>
</button>
<slot />
</div>
</template>
<style>
pre code .line {
display: block;
min-height: 1rem;
}
</style>
<style lang="scss" module>
.wrapper {
position: relative;
}
.icon {
margin: 0 4px -2px 0;
}
.language {
cursor: pointer;
position: absolute;
right: 0;
top: 0;
padding: 4px 8px;
border-radius: 0 4px 0 4px;
text-transform: uppercase;
font-size: 0.75rem;
font-weight: 400;
background: var(--color-code-language-background);
color: var(--color-code-language-name);
user-select: none;
opacity: 0;
transition: all 250ms;
display: flex;
align-items: center;
justify-content: center;
.wrapper:hover & {
opacity: 0.7;
&:hover {
opacity: 1;
}
}
&:active {
transform: scale(1.1);
opacity: 1;
}
}
</style>

View file

@ -0,0 +1,34 @@
<template>
<masonry-wall
:items="parentItems"
:ssr-columns="1"
:column-width="300"
:gap="10"
>
<template #default="{ item }">
<div :class="$style.row">
<LayoutMainMenuRow
:title="item.title"
:url="item.url"
:children="item.children"
/>
</div>
</template>
</masonry-wall>
</template>
<script setup>
const { data: navigation } = await useAsyncData("navigation", () => {
return fetchContentNavigation();
});
const parentItems = navigation.value.filter(
(it) => it.children && Array.isArray(it.children) && it.children.length > 0
);
</script>
<style lang="scss" module>
.row {
margin-bottom: 10px;
}
</style>

View file

@ -0,0 +1,51 @@
<template>
<article>
<h1>{{ item?.title }}</h1>
<ul v-if="item?.children?.length" :class="$style.list">
<li v-for="child in item.children" :key="item._id">
<NuxtLink :to="child._path">{{ child.title }}</NuxtLink>
</li>
</ul>
</article>
</template>
<script lang="ts" setup>
import { NavItem } from "@nuxt/content/dist/runtime/types";
interface Props {
url: string;
}
const findDeep = (items: NavItem[], path: string[]) => {
const item = items.find((it) => it._path.endsWith(path[0]));
if (!item || (path.length > 1 && !item.children?.length)) {
return null;
}
return path.length === 1
? item
: findDeep(item.children, path.slice(1, path.length));
};
const props = defineProps<Props>();
const { data: navigation } = await useAsyncData("navigation", () => {
return fetchContentNavigation();
});
const segments = props.url.split("/").filter((it) => it);
const item = findDeep(navigation.value, segments);
</script>
<style lang="scss" module>
.list {
margin: 0;
padding: 0 20px;
li a {
text-decoration: none;
}
}
</style>

12
components/icons/Moon.vue Normal file
View file

@ -0,0 +1,12 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="48"
width="48"
viewBox="0 0 48 48"
>
<path
d="M17.85 7.55q-.7 0-1.45.075t-1.3.125q3.05 3.45 4.675 7.6Q21.4 19.5 21.4 24t-1.625 8.65Q18.15 36.8 15.15 40.2q.5.1 1.225.175.725.075 1.525.075 6.8 0 11.6-4.775T34.3 24q0-6.9-4.825-11.675T17.85 7.55Zm.25-1.5q3.6 0 6.85 1.375 3.25 1.375 5.65 3.8 2.4 2.425 3.8 5.7 1.4 3.275 1.4 7.025 0 3.75-1.425 7.05t-3.8 5.75Q28.2 39.2 24.95 40.575t-6.9 1.375q-1.65 0-3.125-.275t-2.675-.725q3.65-3.35 5.65-7.725 2-4.375 2-9.225 0-4.75-2-9.175-2-4.425-5.65-7.775 1.15-.45 2.675-.725Q16.45 6.05 18.1 6.05ZM21.4 24Z"
/>
</svg>
</template>

12
components/icons/Sun.vue Normal file
View file

@ -0,0 +1,12 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="48"
width="48"
viewBox="0 0 48 48"
>
<path
d="M24 30.45q2.65 0 4.55-1.875T30.45 24q0-2.65-1.875-4.55T24 17.55q-2.65 0-4.55 1.875T17.55 24q0 2.65 1.875 4.55T24 30.45ZM24 32q-3.35 0-5.675-2.325Q16 27.35 16 24q0-3.35 2.325-5.675Q20.65 16 24 16q3.35 0 5.675 2.325Q32 20.65 32 24q0 3.35-2.325 5.675Q27.35 32 24 32ZM3.75 24.75q-.3 0-.525-.225Q3 24.3 3 24q0-.35.225-.55.225-.2.525-.2h5.5q.3 0 .525.225Q10 23.7 10 24q0 .35-.225.55-.225.2-.525.2Zm35 0q-.3 0-.525-.225Q38 24.3 38 24q0-.35.225-.55.225-.2.525-.2h5.5q.3 0 .525.225Q45 23.7 45 24q0 .35-.225.55-.225.2-.525.2ZM24 10q-.35 0-.55-.225-.2-.225-.2-.525v-5.5q0-.3.225-.525Q23.7 3 24 3q.35 0 .55.225.2.225.2.525v5.5q0 .3-.225.525Q24.3 10 24 10Zm0 35q-.35 0-.55-.225-.2-.225-.2-.525v-5.5q0-.3.225-.525Q23.7 38 24 38q.35 0 .55.225.2.225.2.525v5.5q0 .3-.225.525Q24.3 45 24 45ZM13.05 14.05l-3.2-3.1q-.25-.2-.225-.525.025-.325.225-.575.25-.25.55-.25.3 0 .55.25L14.1 13q.25.25.25.55 0 .3-.25.55-.2.2-.5.2t-.55-.25Zm24 24.1L33.9 35q-.25-.25-.25-.55 0-.3.3-.55.15-.25.45-.225.3.025.55.275l3.2 3.1q.25.2.225.525-.025.325-.225.575-.25.25-.55.25-.3 0-.55-.25ZM33.9 14.1q-.25-.2-.225-.5.025-.3.275-.55l3.1-3.2q.2-.25.525-.225.325.025.575.225.25.25.25.55 0 .3-.25.55L35 14.1q-.25.25-.55.25-.3 0-.55-.25ZM9.85 38.15q-.25-.25-.25-.55 0-.3.25-.55L13 33.9q.25-.25.55-.25.3 0 .55.25.2.2.2.5t-.25.55l-3.1 3.2q-.25.25-.55.25-.3 0-.55-.25ZM24 24Z"
/>
</svg>
</template>

View file

@ -0,0 +1,26 @@
<template>
<footer :class="[$style.footer, $attrs.class]">
<div>btw, have a nice day</div>
<div :class="$style.filler" />
<div>
(2018 - {{ new Date().getFullYear() }})
<NuxtLink to="https://github.com/muerwre/" target="_blank"
>muerwre</NuxtLink
>
</div>
</footer>
</template>
<style lang="scss" module>
.footer {
color: var(--color-text-secondary);
font-size: 0.8rem;
display: flex;
flex-direction: row;
width: 100%;
}
.filler {
flex: 1;
}
</style>

View file

@ -0,0 +1,36 @@
<template>
<nav>
<div :class="$style.section_title">Reference</div>
<div v-for="item in parentItems" key="item._path" :class="$style.row">
<LayoutMainMenuRow
:title="item.title"
:url="item._path"
:children="item.children"
/>
</div>
</nav>
</template>
<script setup>
const { data: navigation } = await useAsyncData("navigation", () => {
return fetchContentNavigation();
});
const parentItems = navigation.value.filter(
(it) => it.children && Array.isArray(it.children) && it.children.length > 0
);
</script>
<style lang="scss" module>
.section_title {
font-family: var(--family-roboto-slab);
font-weight: 600;
margin: 2rem 0 1.5rem;
font-size: 1.6rem;
}
.row {
margin-bottom: 15px;
}
</style>

View file

@ -0,0 +1,158 @@
<template>
<div
v-if="children?.length || !url"
:class="[$style.container, { [$style.secondary]: secondary }]"
>
<div :class="$style.heading">
{{ title }}
</div>
<div :class="$style.children">
<LayoutMainMenuRow
v-for="item in children"
key="item._path"
:title="item.title"
:url="item._path"
:children="item.children"
secondary
/>
</div>
</div>
<div v-else :class="$style.row">
<NuxtLink :to="url" :class="$style.link" :exactActiveClass="$style.active"
>{{ title }}
</NuxtLink>
</div>
</template>
<script lang="ts" setup>
interface Props {
title: string;
url?: string;
children?: Child[];
secondary?: boolean;
}
interface Child {
title: string;
_path: string;
children: Child[];
}
defineProps<Props>();
</script>
<script lang="ts">
export default defineComponent({
mounted() {
const active = document.querySelector(
`.${this.$style.link}.${this.$style.active}`
);
if (!active) return;
active?.scrollIntoView({ block: "center" });
},
});
</script>
<style lang="scss" module>
@mixin tree {
&::before {
content: " ";
background-color: var(--color-menu-line);
width: 10px;
height: 1px;
position: absolute;
top: 0.6em;
left: -17px;
}
}
.container {
position: relative;
&.secondary {
padding: 7px 2px 0;
&::before {
content: " ";
background-color: var(--color-menu-line);
width: 1px;
position: absolute;
top: -22px;
bottom: 13px;
left: -16px;
}
&:first-child::before {
top: -4px;
}
&:last-child::before {
bottom: auto;
height: 40px;
}
}
}
.row {
padding: 3px 2px;
position: relative;
&::before {
content: " ";
background-color: var(--color-menu-line);
width: 1px;
position: absolute;
top: -14px;
bottom: 13px;
left: -16px;
}
&:first-child::before {
top: -4px;
}
&:last-child::before {
bottom: auto;
height: 30px;
}
&:only-child::before {
height: 19px;
}
}
.heading {
font-weight: 600;
display: flex;
align-items: center;
text-transform: uppercase;
position: relative;
color: var(--color-menu-title);
.secondary & {
@include tree;
}
}
.link {
color: var(--color-menu-link);
text-decoration: none;
line-height: 1.4em;
position: relative;
@include tree;
&.active {
color: var(--color-menu-link-active);
font-weight: bold;
}
}
.children {
padding: 0 0 0 16px;
margin: 10px 3px;
position: relative;
}
</style>

View file

@ -0,0 +1,67 @@
<template>
<button :class="[$attrs.class, $style.button]">
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
fill="#ffffff"
:class="[$style.hamburger, { [$style.active]: active }]"
>
<rect x="0" y="3" width="24" height="2" />
<rect x="0" y="11" width="24" height="2" />
<rect x="0" y="19" width="24" height="2" />
</svg>
</button>
</template>
<script lang="ts" setup>
interface Props {
active?: boolean;
}
defineProps<Props>();
</script>
<style lang="scss" module>
.button {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
.hamburger {
fill: var(--color-text);
cursor: pointer;
transition: all 250ms;
&:hover {
fill: var(--color-link);
}
& > rect {
transition: transform 250ms;
}
&.active {
& > rect:nth-child(1) {
transform: rotate(45deg);
transform-origin: 2px 8px;
}
& > rect:nth-child(2) {
transform: scaleX(0);
transform-origin: 13px 0;
transition-delay: 100ms;
}
& > rect:nth-child(3) {
transform: rotate(-45deg);
transform-origin: 3px 16px;
transition-delay: 50ms;
}
}
}
</style>

View file

@ -0,0 +1,52 @@
<template>
<button
@click="toggleTheme"
:class="[$attrs.class, $style.button, { [$style.visible]: visible }]"
>
<ClientOnly>
<IconsMoon fill="currentColor" width="32" height="32" v-if="isDark" />
<IconsSun fill="currentColor" width="32" height="32" v-if="!isDark" />
</ClientOnly>
</button>
</template>
<script lang="ts" setup>
const visible = ref(false);
onMounted(() => {
visible.value = true;
});
</script>
<script lang="ts">
export default defineComponent({
methods: {
toggleTheme() {
this.$colorMode.preference =
this.$colorMode.preference === "dark" ? "light" : "dark";
},
},
computed: {
isDark() {
return this.$colorMode.preference === "dark";
},
},
});
</script>
<style lang="scss" module>
.button {
color: var(--color-text-secondary);
cursor: pointer;
transform: scale(0) rotate(180deg);
transition: all 0.25s ease-out;
&.visible {
transform: scale(1) rotate(0);
}
&:hover {
color: var(--color-text);
}
}
</style>

View file

@ -0,0 +1,14 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#ffffff"
>
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"
/>
</svg>
</template>

7
custom.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
import { decl } from "postcss";
import { NitroAppPlugin, NitroApp } from "nitropack";
declare module "*.svg" {
const content: string;
export default content;
}

11
docker/Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM node:16-alpine as builder
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn
COPY . .
RUN yarn generate
FROM nginx
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/dist /usr/share/nginx/html

54
docker/nginx.conf Normal file
View file

@ -0,0 +1,54 @@
worker_processes 4;
events {
worker_connections 1024;
}
http {
server {
listen 80;
root /usr/share/nginx/html;
include /etc/nginx/mime.types;
gzip on;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain application/xml application/javascript;
## All static files will be served directly.
location ~* ^.+\.(?:css|cur|js|jpe?g|gif|htc|ico|png|xml|otf|ttf|eot|woff|woff2|svg)$ {
access_log off;
expires 1d;
add_header Cache-Control public;
gzip_static on;
## No need to bleed constant updates. Send the all shebang in one
## fell swoop.
tcp_nodelay off;
## Set the OS file cache.
open_file_cache max=3000 inactive=120s;
open_file_cache_valid 45s;
open_file_cache_min_uses 2;
open_file_cache_errors off;
}
location / {
# try_files $uri @index;
index index.html;
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
etag off;
}
location @index {
add_header Cache-Control "no-store, no-cache, must-revalidate";
expires -1;
try_files /index.html =404;
}
}
}

54
layouts/content.vue Normal file
View file

@ -0,0 +1,54 @@
<template>
<div :class="$style.wrapper">
<div :class="$style.content">
<LayoutThemeToggle :class="$style.theme_toggle" />
<slot />
</div>
<LayoutFooter :class="$style.footer" />
</div>
</template>
<style lang="scss" module>
@import "~~/assets/css/mixins";
.wrapper {
height: 100%;
display: flex;
flex-direction: column;
}
.content {
flex: 1;
padding: 40px 120px;
@include desktop {
padding: 40px 40px 20px 40px;
}
@include tablet {
padding: 40px 20px 20px 20px;
}
}
.footer {
margin-top: 60px;
padding: 10px;
border-top: 1px solid var(--color-line);
}
.theme_toggle {
position: absolute;
top: 20px;
right: 20px;
stroke: currentColor;
stroke-width: 0.5px;
@include tablet {
stroke-width: 1.5px;
right: 16px;
top: 16px;
}
}
</style>

165
layouts/default.vue Normal file
View file

@ -0,0 +1,165 @@
<template>
<div :class="$style.grid">
<LayoutMainMenuToggle
:active="menuVisible"
:class="[
$style.menu_toggle,
{ [$style.shifted]: menuShifted, [$style.active]: menuVisible },
]"
@click="toggleMenu"
/>
<div
:class="[$style.sidebar, { [$style.active]: menuVisible }]"
ref="sidebar"
>
<div :class="$style.menu">
<LayoutMainMenu />
</div>
</div>
<div :class="$style.main">
<NuxtLayout name="content">
<slot />
</NuxtLayout>
</div>
</div>
</template>
<script>
import { disableBodyScroll, clearAllBodyScrollLocks } from "body-scroll-lock";
export default defineComponent({
setup() {
const scrollTop = ref(0);
const onScroll = () => {
scrollTop.value = window.scrollY;
};
onMounted(() => addEventListener("scroll", onScroll));
onUnmounted(() => removeEventListener("scroll", onScroll));
const menuShifted = computed(() => scrollTop.value > 60);
return { menuShifted, scrollTop };
},
data() {
return {
menuVisible: false,
};
},
methods: {
toggleMenu() {
this.$data.menuVisible = !this.$data.menuVisible;
},
},
watch: {
$route() {
if (!this.menuVisible) return;
nextTick(() => this.toggleMenu());
},
menuVisible(val) {
if (val) {
disableBodyScroll(this.$refs.sidebar);
}
clearAllBodyScrollLocks();
},
},
});
</script>
<style module lang="scss">
@import "~~/assets/css/mixins";
.grid {
display: grid;
grid-template-columns: 360px auto;
width: 100vw;
@include desktop {
grid-template-columns: 33vw auto;
}
@include tablet {
grid-template-columns: auto;
}
}
.main {
min-width: 0;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
}
.sidebar {
background-color: var(--color-menu-background);
@include tablet {
position: fixed;
z-index: 2;
background-color: var(--color-menu-overlay-background);
width: 100%;
visibility: hidden;
opacity: 0;
transition: opacity 0.5s;
&.active {
visibility: visible;
opacity: 1;
}
}
}
.menu {
padding: 40px 30px;
position: sticky;
top: 0;
height: 100vh;
overflow: auto;
min-width: 0;
background-color: var(--color-menu-background);
max-width: 400px;
@include tablet {
transition: transform 0.25s 0.1s;
transform: translate(-40px, 0);
.active & {
transform: translate(0, 0);
}
}
}
.menu_toggle {
position: fixed;
left: 13px;
top: 13px;
z-index: 4;
visibility: hidden;
transform: translate(0, 0);
transition: all 250ms;
border-radius: 0 0 8px 0;
@include tablet {
transform: translate(0, 0);
right: 0;
visibility: visible;
}
&.shifted,
&.active {
transform: translate(-13px, -13px);
}
&.shifted {
background: var(--color-menu-background);
}
}
.footer {
margin-top: 40px;
}
</style>

50
nuxt.config.ts Normal file
View file

@ -0,0 +1,50 @@
export default defineNuxtConfig({
alias: {
"~": "/<rootDir>",
},
modules: ["@nuxt/content", "@nuxtjs/color-mode"],
typescript: {
shim: false,
typeCheck: true,
},
content: {
navigation: {
fields: ["blblblb"],
},
highlight: {
theme: {
default: "github-dark",
light: "solarized-light",
},
preload: [
"shell",
"c",
"go",
"graphql",
"scss",
"shell",
"sh",
"docker",
"typescript",
"javascript",
"nginx",
"bash",
"yaml",
"sh",
],
},
},
target: "static",
css: ["@/assets/css/main.scss"],
head: {
link: [{ rel: "icon", type: "image/png", href: "/favicon.png" }],
},
colorMode: {
preference: "dark",
classSuffix: "",
storageKey: "nuxt-color-mode",
},
app: {
buildAssetsDir: "nuxt/",
},
});

28
package.json Normal file
View file

@ -0,0 +1,28 @@
{
"private": true,
"typings": "*.d.ts",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"tsc": "nuxi typecheck"
},
"devDependencies": {
"@lewebsimple/nuxt3-svg": "^0.1.1",
"@nuxt/content": "^2.2.0",
"@nuxtjs/color-mode": "^3.1.8",
"add": "^2.0.6",
"nuxt": "3.0.0-rc.12",
"typescript": "^4.8.4",
"vue-tsc": "^1.0.9",
"yarn": "^1.22.19"
},
"dependencies": {
"@yeger/vue-masonry-wall": "^3.2.14",
"body-scroll-lock": "^4.0.0-beta.0",
"sass": "^1.55.0",
"vue-masonry-css": "^1.0.3"
}
}

30
pages/[...slug].vue Normal file
View file

@ -0,0 +1,30 @@
<template>
<main>
<ContentDoc>
<template v-slot="{ doc }">
<h1>{{ doc.title }}</h1>
<article>
<ContentRenderer :value="doc" />
</article>
</template>
<template v-slot:not-found="{ props: { path } }">
<HomeReference :url="path" />
</template>
</ContentDoc>
</main>
</template>
<script setup lang="ts">
useHead({
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} • Obsidian Garden` : "Obsidian Garden";
},
});
</script>
<script lang="ts">
export default {
scrollToTop: true,
};
</script>

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,10 @@
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("content:file:beforeParse", (file) => {
if (file._id.endsWith(".md")) {
file.body = file.body.replace(
/==([^=]+)==/gs,
`<span class="highlight">$1</span>`
);
}
});
});

4
tsconfig.json Normal file
View file

@ -0,0 +1,4 @@
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

5491
yarn.lock Normal file

File diff suppressed because it is too large Load diff