diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ef3e1f..8db8bbe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} type=raw,value={{branch}}-{{sha}}-{{date 'X'}},enable=${{ startsWith(github.ref, 'refs/heads') }} type=semver,pattern={{version}},prefix=v - type=semver,pattern={{major}}.{{minor}},prefix=v + type=semver,pattern=v{{major}}.{{minor}},prefix=v - name: Build and push image uses: docker/build-push-action@v3 diff --git a/README.md b/README.md index 83a8f83..37e8573 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ It's super easy to install and use. - 🤩 [Editor.js](https://editorjs.io/) ecosystem powered - 📂 Docs nesting — create any structure you need -- 💎 Static rendering - 📱 Nice look on Desktop and Mobile - 🔥 Beautiful page URLs. Human-readable and SEO-friendly. - 🦅 [Hawk](https://hawk.so/?from=docs-demo) is hunting. Errors tracking integrated diff --git a/package.json b/package.json index 4e6ae91..c9ff0c8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codex.docs", "license": "Apache-2.0", - "version": "2.2.3", + "version": "v2.0.0-rc.4", "type": "module", "bin": { "codex.docs": "dist/backend/app.js" diff --git a/src/backend/build-static.ts b/src/backend/build-static.ts index ca0894a..d0691e0 100644 --- a/src/backend/build-static.ts +++ b/src/backend/build-static.ts @@ -13,7 +13,6 @@ import fse from 'fs-extra'; import appConfig from './utils/appConfig.js'; import Aliases from './controllers/aliases.js'; import Pages from './controllers/pages.js'; -import { downloadFavicon } from './utils/downloadFavicon.js'; /** * Build static pages from database @@ -46,42 +45,14 @@ export default async function buildStatic(): Promise { }); } - if (config.overwrite) { - console.log('Removing old static files'); - await fse.remove(distPath); - } + console.log('Removing old static files'); + await fse.remove(distPath); console.log('Building static files'); const pagesOrder = await PagesOrder.getAll(); const allPages = await Page.getAll(); - try { - console.log('Create dist folder'); - await mkdirp(distPath); - } catch (e) { - console.log('Error while creating dist folder', e); - - return; - } - - console.log('Copy public directory'); - const publicDir = path.resolve(dirname, '../../public'); - - console.log(`Copy from ${publicDir} to ${distPath}`); - - try { - await fse.copy(publicDir, distPath); - console.log('Public directory copied'); - } catch (e) { - console.log('Error while copying public directory'); - console.error(e); - } - - const favicon = appConfig.favicon ? await downloadFavicon(appConfig.favicon, distPath, '') : { - destination: '/favicon.png', - type: 'image/png', - }; - + await mkdirp(distPath); /** * Renders single page @@ -91,11 +62,6 @@ export default async function buildStatic(): Promise { */ async function renderPage(page: Page, isIndex?: boolean): Promise { console.log(`Rendering page ${page.uri}`); - const pageUri = page.uri; - - if (!pageUri) { - throw new Error('Page uri is not defined'); - } const pageParent = await page.getParent(); const pageId = page._id; @@ -106,30 +72,16 @@ export default async function buildStatic(): Promise { const previousPage = await PagesFlatArray.getPageBefore(pageId); const nextPage = await PagesFlatArray.getPageAfter(pageId); const menu = createMenuTree(parentIdOfRootPages, allPages, pagesOrder, 2); - const result = await renderTemplate('./views/pages/page.twig', { page, pageParent, previousPage, nextPage, menu, - favicon, config: appConfig.frontend, }); - let filename: string; - - if (isIndex) { - filename = 'index.html'; - } else if (config?.pagesInsideFolders) { // create folder for each page if pagesInsideFolders is true - const pagePath = path.resolve(distPath, pageUri); - - await mkdirp(pagePath); - - filename = path.resolve(pagePath, 'index.html'); - } else { - filename = `${page.uri}.html`; - } + const filename = (isIndex || page.uri === '') ? 'index.html' : `${page.uri}.html`; await fs.writeFile(path.resolve(distPath, filename), result); console.log(`Page ${page.uri} rendered`); @@ -159,16 +111,15 @@ export default async function buildStatic(): Promise { await renderPage(page); } - // Check if index page is enabled - if (config.indexPage.enabled) { - await renderIndexPage(config.indexPage.uri); - } + await renderIndexPage(config.indexPageUri); console.log('Static files built'); + console.log('Copy public directory'); + await fse.copy(path.resolve(dirname, '../../public'), distPath); + if (appConfig.uploads.driver === 'local') { console.log('Copy uploads directory'); await fse.copy(path.resolve(cwd, appConfig.uploads.local.path), path.resolve(distPath, 'uploads')); - console.log('Uploads directory copied'); } } diff --git a/src/backend/utils/appConfig.ts b/src/backend/utils/appConfig.ts index 34a5e61..c824d49 100644 --- a/src/backend/utils/appConfig.ts +++ b/src/backend/utils/appConfig.ts @@ -90,14 +90,7 @@ const FrontendConfig = z.object({ */ const StaticBuildConfig = z.object({ outputDir: z.string(), // Output directory for static build - overwrite: z.boolean().optional() // Overwrite output directory - .default(true), - pagesInsideFolders: z.boolean().optional() // Create separate folder for each page - .default(true), - indexPage: z.object({ - enabled: z.boolean(), // Is index page enabled - uri: z.string(), // Index page uri - }), + indexPageUri: z.string(), // URI for index page to render }); export type StaticBuildConfig = z.infer; diff --git a/src/backend/utils/downloadFavicon.ts b/src/backend/utils/downloadFavicon.ts index ed10cdb..73aa2c0 100644 --- a/src/backend/utils/downloadFavicon.ts +++ b/src/backend/utils/downloadFavicon.ts @@ -1,5 +1,5 @@ import path from 'path'; -import fs from 'fs/promises'; +import fs from 'fs'; import fetch, { RequestInit } from 'node-fetch'; /** @@ -32,10 +32,9 @@ function checkIsUrl(str: string): boolean { * * @param destination - url or path of favicon * @param faviconFolder - folder to save favicon - * @param subRoute - subroute from which the favicon will be served * @returns { Promise } - Promise with data about favicon */ -export async function downloadFavicon(destination: string, faviconFolder: string, subRoute = '/favicon'): Promise { +export async function downloadFavicon(destination: string, faviconFolder: string): Promise { // Check of destination is empty if (!destination) { throw Error('Favicon destination is empty'); @@ -49,10 +48,8 @@ export async function downloadFavicon(destination: string, faviconFolder: string // Check if string is url if (!checkIsUrl(destination)) { - await fs.copyFile(destination, path.join(faviconFolder, filename)); - return { - destination: `${subRoute}/${filename}`, + destination: `/${filename}`, type: `image/${format}`, } as FaviconData; } @@ -75,10 +72,14 @@ export async function downloadFavicon(destination: string, faviconFolder: string const filePath = path.join(faviconFolder, `favicon.${format}`); // Save file - await fs.writeFile(filePath, fileData); + await fs.writeFile(filePath, fileData, (err) => { + if (err) { + console.log(err); + } + }); return { - destination: `${subRoute}/favicon.${format}`, + destination: `/favicon/favicon.${format}`, type: `image/${format}`, } as FaviconData; } diff --git a/src/backend/views/components/copy-button.twig b/src/backend/views/components/copy-button.twig deleted file mode 100644 index 2db7ee1..0000000 --- a/src/backend/views/components/copy-button.twig +++ /dev/null @@ -1,25 +0,0 @@ -{# - Reusable copy button component. - Available props: - - ariaLabel: label for better accessibility - - class: additional class for the button - - textToCopy: text to be copied to the clipboard (use '#' for anchor links) - - Usage examples: - {% include 'components/copy-button.twig' with { textToCopy: 'Lorem ipsum dolor' } %} - {% include 'components/copy-button.twig' with { textToCopy: '#anchor-link-dolor' } %} -#} - -{% set attrNameForTextToCopy = 'data-text-to-copy' %} - -{% set ariaLabel = ariaLabel ?? 'Copy to the Clipboard' %} - -{% set mainTag = 'button' %} -{% set mainClass = 'copy-button' %} - -<{{ mainTag }} class="{{ mainClass }} {{ class ?? '' }}" aria-label="{{ ariaLabel }}" {{ attrNameForTextToCopy }}="{{ textToCopy }}"> -
-
{{ svg('copy') }}
-
{{ svg('check') }}
-
- diff --git a/src/backend/views/pages/blocks/code.twig b/src/backend/views/pages/blocks/code.twig index 9ed997e..663ff49 100644 --- a/src/backend/views/pages/blocks/code.twig +++ b/src/backend/views/pages/blocks/code.twig @@ -1,12 +1,4 @@
-
-
{{ code | escape }}
-
- {% - include '../../components/copy-button.twig' with { - ariaLabel: 'Copy Code to Clipboard', - class: 'block-code__copy-button', - textToCopy: code | escape, - } - %} +
{{ code|escape }}
+ diff --git a/src/backend/views/pages/blocks/header.twig b/src/backend/views/pages/blocks/header.twig index d854dd9..f069652 100644 --- a/src/backend/views/pages/blocks/header.twig +++ b/src/backend/views/pages/blocks/header.twig @@ -1,12 +1,11 @@ - - {% - include '../../components/copy-button.twig' with { - ariaLabel: 'Copy Link to the ' ~ text, - class: 'block-header__copy-button', - textToCopy: '#' ~ text | urlify, - } - %} + + + {{ text }} + diff --git a/src/frontend/js/classes/table-of-content.js b/src/frontend/js/classes/table-of-content.js index 9e46e94..b4900fb 100644 --- a/src/frontend/js/classes/table-of-content.js +++ b/src/frontend/js/classes/table-of-content.js @@ -193,7 +193,7 @@ export default class TableOfContent { const linkWrapper = $.make('li', this.CSS.tocElementItem); const linkBlock = $.make('a', null, { - innerText: tag.innerText.trim(), + innerText: tag.innerText, href: `${linkTarget}`, }); diff --git a/src/frontend/js/modules/page.js b/src/frontend/js/modules/page.js index 126b5fd..00bbaf9 100644 --- a/src/frontend/js/modules/page.js +++ b/src/frontend/js/modules/page.js @@ -20,9 +20,10 @@ export default class Page { */ static get CSS() { return { - copyButton: 'copy-button', - copyButtonCopied: 'copy-button__copied', page: 'page', + copyLinkBtn: 'block-header__copy-link', + header: 'block-header--anchor', + headerLinkCopied: 'block-header--link-copied', }; } @@ -34,15 +35,11 @@ export default class Page { this.tableOfContent = this.createTableOfContent(); /** - * Add click event listener + * Add click event listener to capture copy link button clicks */ const page = document.querySelector(`.${Page.CSS.page}`); - page.addEventListener('click', (event) => { - if (event.target.classList.contains(Page.CSS.copyButton)) { - this.handleCopyButtonClickEvent(event); - } - }); + page.addEventListener('click', this.copyAnchorLinkIfNeeded); } /** @@ -72,7 +69,10 @@ export default class Page { try { // eslint-disable-next-line no-new new TableOfContent({ - tagSelector: '.block-header', + tagSelector: + 'h2.block-header--anchor,' + + 'h3.block-header--anchor,' + + 'h4.block-header--anchor', appendTo: document.getElementById('layout-sidebar-right'), }); } catch (error) { @@ -81,31 +81,27 @@ export default class Page { } /** - * Handles copy button click events + * Checks if 'copy link' button was clicked and copies the link to clipboard * - * @param {Event} e - Event Object. - * @returns {Promise} + * @param e - click event */ - async handleCopyButtonClickEvent({ target }) { - if (target.classList.contains(Page.CSS.copyButtonCopied)) return; + copyAnchorLinkIfNeeded = async (e) => { + const copyLinkButtonClicked = e.target.closest(`.${Page.CSS.copyLinkBtn}`); - let textToCopy = target.getAttribute('data-text-to-copy'); - if (!textToCopy) return; - - // Check if text to copy is an anchor link - if (/^#\S*$/.test(textToCopy)) - textToCopy = window.location.origin + window.location.pathname + textToCopy; - - try { - await copyToClipboard(textToCopy); - - target.classList.add(Page.CSS.copyButtonCopied); - target.addEventListener('mouseleave', () => { - setTimeout(() => target.classList.remove(Page.CSS.copyButtonCopied), 5e2); - }, { once: true }); - - } catch (error) { - console.error(error); // @todo send to Hawk + if (!copyLinkButtonClicked) { + return; } + + const header = e.target.closest(`.${Page.CSS.header}`); + const link = header.querySelector('a').href; + + await copyToClipboard(link); + header.classList.add(Page.CSS.headerLinkCopied); + + header.addEventListener('mouseleave', () => { + setTimeout(() => { + header.classList.remove(Page.CSS.headerLinkCopied); + }, 500); + }, { once: true }); } } diff --git a/src/frontend/styles/components/copy-button.pcss b/src/frontend/styles/components/copy-button.pcss deleted file mode 100644 index 2ec2e42..0000000 --- a/src/frontend/styles/components/copy-button.pcss +++ /dev/null @@ -1,103 +0,0 @@ -.copy-button { - position: relative; - width: 28px; - height: 28px; - padding: 0; - border: none; - background: none; - cursor: pointer; - transition: opacity 200ms; - - @media (--can-hover) { - &:hover .copy-button__inner { - background: var(--color-link-hover); - } - } - - &::before { - content: ''; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - border-radius: 100%; - background-color: var(--color-success); - visibility: hidden; - pointer-events: none; - transform: scale(1); - transition: transform 400ms ease-out, opacity 400ms; - } - - &__inner { - @apply --squircle; - - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; - background: white; - pointer-events: none; - } - - &__icon--initial { - display: flex; - transform: translateZ(0); - } - - &__icon--success { - display: none; - width: 24px; - height: 24px; - color: white; - } - - &__copied { - - &::before { - opacity: 0; - visibility: visible; - transform: scale(3.5); - } - - .copy-button__inner, - .copy-button__inner:hover { - background: var(--color-success) !important; - animation: check-square-in 250ms ease-in; - } - - .copy-button__icon--initial { - display: none; - } - - .copy-button__icon--success { - display: flex; - animation: check-sign-in 350ms ease-in forwards; - } - } - - @keyframes check-sign-in { - from { - transform: scale(.7); - } - 80% { - transform: scale(1.1); - } - to { - transform: none; - } - } - - @keyframes check-square-in { - from { - transform: scale(1.05); - } - 80% { - transform: scale(.96); - } - to { - transform: none; - } - } -} diff --git a/src/frontend/styles/components/header.pcss b/src/frontend/styles/components/header.pcss index 3c62720..0b43e9e 100644 --- a/src/frontend/styles/components/header.pcss +++ b/src/frontend/styles/components/header.pcss @@ -40,10 +40,7 @@ html { @media (--not-mobile) { padding: 4px 10px; - - &:hover { - @apply --squircle; - } + @apply --squircle; } &:hover { diff --git a/src/frontend/styles/components/page.pcss b/src/frontend/styles/components/page.pcss index 20a408d..7109556 100644 --- a/src/frontend/styles/components/page.pcss +++ b/src/frontend/styles/components/page.pcss @@ -125,15 +125,79 @@ pointer-events: none; } - &__copy-button { - margin-right: 8px; + &--link-copied { + .block-header__copy-link, + .block-header__copy-link:hover { + background: var(--color-success); + opacity: 1; + animation: check-square-in 250ms ease-in; + pointer-events: none; + } + + .block-header__copy-link-icon--initial { + display: none; + } + + .block-header__copy-link-icon--success { + display: flex; + animation: check-sign-in 350ms ease-in forwards; + } + + .block-header__copy-link-splash { + opacity: 0; + visibility: visible; + transform: scale(3.5); + } + } + + &__copy-link-splash { + position: absolute; + left: 0; + width: 28px; + height: 28px; + background-color: var(--color-success); + transform: scale(1); + border-radius: 100%; + transition: transform 400ms ease-out, opacity 400ms; + visibility: hidden; + } + + &__copy-link-icon--success { + width: 24px; + height: 24px; + display: none; + color: white; + } + + &__copy-link-icon--initial { + display: flex; + justify-content: center; + align-items: center; + } + + &__copy-link { + width: 28px; + height: 28px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + @apply --squircle; color: var(--color-text-second); opacity: 0; + margin-right: 8px; + + @media (--can-hover) { + &:hover { + background: var(--color-link-hover); + } + } } @media (--can-hover) { &:hover { - .block-header__copy-button { + .block-header__copy-link { opacity: 1; } } @@ -142,6 +206,30 @@ .inline-code { line-height: inherit; } + + @keyframes check-sign-in { + from { + transform: scale(.7); + } + 80% { + transform: scale(1.1); + } + to { + transform: none; + } + } + + @keyframes check-square-in { + from { + transform: scale(1.05); + } + 80% { + transform: scale(.96); + } + to { + transform: none; + } + } } /** @@ -149,18 +237,8 @@ * ================== */ .block-code { - position: relative; - - &:hover { - .block-code__copy-button { - opacity: 1; - } - } - - &__wrapper { - @apply --text-code-block; - @apply --squircle; - } + @apply --text-code-block; + @apply --squircle; &__content { display: inline-block !important; @@ -179,13 +257,6 @@ } } - &__copy-button { - position: absolute; - top: 10px; - right: 10px; - opacity: 0; - } - .hljs-params { color: var(--color-code-params); } @@ -509,3 +580,4 @@ } } } + diff --git a/src/frontend/styles/components/sidebar.pcss b/src/frontend/styles/components/sidebar.pcss index 04f6f83..23c2493 100644 --- a/src/frontend/styles/components/sidebar.pcss +++ b/src/frontend/styles/components/sidebar.pcss @@ -1,4 +1,5 @@ .docs-sidebar { + width: 100vw; /* Bottom and Left coord of the "Hide Sidebar" toggler */ --hide-sidebar-toggler-offset: 11px; @@ -182,16 +183,13 @@ transition-property: background-color; transition-duration: 0.1s; + @apply --squircle; + &--selected { border-radius: 8px; /* border using box-shadow which doesn't increase the height */ box-shadow: 0 0 0 2px rgba(147, 166, 233, 0.5) inset; } - - &--active, - &:hover { - @apply --squircle; - } } &__section-title > span, diff --git a/src/frontend/styles/components/table-of-content.pcss b/src/frontend/styles/components/table-of-content.pcss index a3f5674..ab3c921 100644 --- a/src/frontend/styles/components/table-of-content.pcss +++ b/src/frontend/styles/components/table-of-content.pcss @@ -31,10 +31,7 @@ gap: 2px; &-item { - &:hover, - &--active { - @apply --squircle; - } + @apply --squircle; &:hover { background-color: var(--color-link-hover); diff --git a/src/frontend/styles/layout.pcss b/src/frontend/styles/layout.pcss index 1c5e56c..8f6e899 100644 --- a/src/frontend/styles/layout.pcss +++ b/src/frontend/styles/layout.pcss @@ -30,7 +30,7 @@ @media (--desktop) { max-width: min( calc(var(--layout-width-main-col) + var(--max-space-between-cols) + var(--layout-sidebar-width)), - calc(100% - var(--layout-sidebar-width)) + calc(100vw - var(--layout-sidebar-width)) ); margin-left: max(var(--main-col-min-margin-left), calc(50vw - var(--layout-sidebar-width) - var(--layout-width-main-col) / 2) - var(--layout-padding-horizontal)); margin-right: auto; diff --git a/src/frontend/styles/main.pcss b/src/frontend/styles/main.pcss index d6edcbf..c436ade 100644 --- a/src/frontend/styles/main.pcss +++ b/src/frontend/styles/main.pcss @@ -5,7 +5,6 @@ @import './carbon.pcss'; @import './components/header.pcss'; @import './components/writing.pcss'; -@import './components/copy-button.pcss'; @import './components/page.pcss'; @import './components/greeting.pcss'; @import './components/auth.pcss'; diff --git a/yarn.lock b/yarn.lock index 6a717a9..7ec5712 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2887,7 +2887,6 @@ basic-auth@~2.0.1: big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^2.0.0: version "2.2.0" @@ -3734,7 +3733,6 @@ emoji-regex@^8.0.0: emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== encodeurl@~1.0.2: version "1.0.2" @@ -5064,9 +5062,8 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + version "2.0.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" dependencies: big.js "^5.2.2" emojis-list "^3.0.0"