diff --git a/themes/stack b/themes/stack deleted file mode 160000 index 3e123a3..0000000 --- a/themes/stack +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3e123a30b79b5d52a3a8e88a9dd678fcfd28e418 diff --git a/themes/stack/.ci/build.sh b/themes/stack/.ci/build.sh new file mode 100644 index 0000000..b305610 --- /dev/null +++ b/themes/stack/.ci/build.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +#------------------------------------------------------------------------------ +# @file +# Builds a Hugo site hosted on a Cloudflare Worker. +# +# The Cloudflare Worker automatically installs Node.js dependencies. +#------------------------------------------------------------------------------ + +main() { + DART_SASS_VERSION=1.97.1 + GO_VERSION=1.25.5 + HUGO_VERSION=0.160.1 + NODE_VERSION=24.12.0 + + export TZ=Europe/Oslo + + # Install Dart Sass + echo "Installing Dart Sass ${DART_SASS_VERSION}..." + curl -sLJO "https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz" + tar -C "${HOME}/.local" -xf "dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz" + rm "dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz" + export PATH="${HOME}/.local/dart-sass:${PATH}" + + # Install Go + echo "Installing Go ${GO_VERSION}..." + curl -sLJO "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" + tar -C "${HOME}/.local" -xf "go${GO_VERSION}.linux-amd64.tar.gz" + rm "go${GO_VERSION}.linux-amd64.tar.gz" + export PATH="${HOME}/.local/go/bin:${PATH}" + + # Install Hugo + echo "Installing Hugo ${HUGO_VERSION}..." + curl -sLJO "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" + mkdir "${HOME}/.local/hugo" + tar -C "${HOME}/.local/hugo" -xf "hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" + rm "hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" + export PATH="${HOME}/.local/hugo:${PATH}" + + # Install Node.js + echo "Installing Node.js ${NODE_VERSION}..." + curl -sLJO "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" + tar -C "${HOME}/.local" -xf "node-v${NODE_VERSION}-linux-x64.tar.xz" + rm "node-v${NODE_VERSION}-linux-x64.tar.xz" + export PATH="${HOME}/.local/node-v${NODE_VERSION}-linux-x64/bin:${PATH}" + + # Verify installations + echo "Verifying installations..." + echo Dart Sass: "$(sass --version)" + echo Go: "$(go version)" + echo Hugo: "$(hugo version)" + echo Node.js: "$(node --version)" + + # Configure Git + echo "Configuring Git..." + git config core.quotepath false + if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then + git fetch --unshallow + fi + + # Build the site + echo "Building the site..." + cd demo + hugo --gc --minify --themesDir=/opt/buildhome --theme=repo +} + +set -euo pipefail +main "$@" \ No newline at end of file diff --git a/themes/stack/.github/FUNDING.yml b/themes/stack/.github/FUNDING.yml new file mode 100644 index 0000000..95f0c18 --- /dev/null +++ b/themes/stack/.github/FUNDING.yml @@ -0,0 +1,2 @@ +ko_fi: jimmycai +github: CaiJimmy diff --git a/themes/stack/.github/ISSUE_TEMPLATE/bug_report.yml b/themes/stack/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..81f8833 --- /dev/null +++ b/themes/stack/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,65 @@ +name: Bug Report +description: File a bug report +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! Please provide as much information as possible and make sure you have checked the [documentation](https://stack.jimmycai.com/guide/). + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? Please be as detailed as possible, including screenshots and any other information that might help us reproduce the problem. + placeholder: Tell us what you see! + validations: + required: true + - type: input + id: hugo-version + attributes: + label: Hugo version + description: "What is the version of Hugo you are using? (Note: this theme does not support non-extended version of Hugo)" + placeholder: ex. 0.100.0 + validations: + required: true + - type: input + id: theme-version + attributes: + label: Theme version + description: "What is the version of Stack theme you are using?" + placeholder: ex. 3.12.0 + validations: + required: true + - type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + multiple: true + options: + - Firefox + - Chrome + - Safari + - Microsoft Edge + - type: input + id: browser-info + attributes: + label: More information about the browser + description: "E.g: Browser version, OS version, etc." + placeholder: ex. Chrome 104, Windows 11 + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: input + id: minimal-reproduction-url + attributes: + label: Link to Minimal Reproducible Example + description: | + Use [CaiJimmy/hugo-theme-stack-starter](https://github.com/CaiJimmy/hugo-theme-stack-starter) to create a minimal reproduction of the problem. + A minimal reproduction is required so that others can help debug your issue. If a report is vague (e.g. just a generic error message) and has no reproduction, it may be auto-closed. + placeholder: https://github.com/username/your-repository + validations: + required: true diff --git a/themes/stack/.github/ISSUE_TEMPLATE/config.yml b/themes/stack/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..f120c04 --- /dev/null +++ b/themes/stack/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Discussions + url: https://github.com/CaiJimmy/hugo-theme-stack/discussions + about: Please ask and answer questions here. diff --git a/themes/stack/.gitignore b/themes/stack/.gitignore new file mode 100644 index 0000000..9ff142d --- /dev/null +++ b/themes/stack/.gitignore @@ -0,0 +1,4 @@ +public +resources +assets/jsconfig.json +.hugo_build.lock \ No newline at end of file diff --git a/themes/stack/LICENSE b/themes/stack/LICENSE new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/themes/stack/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/themes/stack/README.md b/themes/stack/README.md new file mode 100644 index 0000000..505ffc5 --- /dev/null +++ b/themes/stack/README.md @@ -0,0 +1,28 @@ +![image](https://user-images.githubusercontent.com/5889006/190859441-141b5f81-8483-40d2-bd96-ebf85616a46d.png) + +# Hugo Theme Stack + +logo + +Card-style Hugo theme designed for bloggers. + +## Quickstart + +Use this template: [CaiJimmy/hugo-theme-stack-starter](https://github.com/CaiJimmy/hugo-theme-stack-starter) + +## Demo + +Check [demo.stack.cai.im](https://demo.stack.cai.im) + +## Documentation + +- [English](https://stack.cai.im) +- [中文](https://stack.cai.im/zh) + +## Copyright + +**Licensed under the GNU General Public License v3.0** + +Please do not remove the "*Theme Stack designed by Jimmy*" text and link. + +If you want to port this theme to another blogging platform, please let me know🙏. diff --git a/themes/stack/archetypes/categories.md b/themes/stack/archetypes/categories.md new file mode 100644 index 0000000..d771b29 --- /dev/null +++ b/themes/stack/archetypes/categories.md @@ -0,0 +1,7 @@ +--- +title: "{{ replace .Name "-" " " | title }}" +image: +style: + background: "#2a9d8f" + color: "#fff" +--- \ No newline at end of file diff --git a/themes/stack/archetypes/default.md b/themes/stack/archetypes/default.md new file mode 100644 index 0000000..6075e9a --- /dev/null +++ b/themes/stack/archetypes/default.md @@ -0,0 +1,12 @@ +--- +title: "{{ replace .Name "-" " " | title }}" +description: +date: {{ .Date }} +image: +math: +license: +comments: true +draft: true +build: + list: always # Change to "never" to hide the page from the list +--- \ No newline at end of file diff --git a/themes/stack/archetypes/tags.md b/themes/stack/archetypes/tags.md new file mode 100644 index 0000000..2fd2fd7 --- /dev/null +++ b/themes/stack/archetypes/tags.md @@ -0,0 +1,5 @@ +--- +title: "{{ replace .Name "-" " " | title }}" +description: +image: +--- \ No newline at end of file diff --git a/themes/stack/assets/icons/archives.svg b/themes/stack/assets/icons/archives.svg new file mode 100644 index 0000000..cd96cbe --- /dev/null +++ b/themes/stack/assets/icons/archives.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/themes/stack/assets/icons/arrow-back.svg b/themes/stack/assets/icons/arrow-back.svg new file mode 100644 index 0000000..0f7c5f4 --- /dev/null +++ b/themes/stack/assets/icons/arrow-back.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/themes/stack/assets/icons/back.svg b/themes/stack/assets/icons/back.svg new file mode 100644 index 0000000..ee52db4 --- /dev/null +++ b/themes/stack/assets/icons/back.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/themes/stack/assets/icons/brand-github.svg b/themes/stack/assets/icons/brand-github.svg new file mode 100644 index 0000000..1fe7e0b --- /dev/null +++ b/themes/stack/assets/icons/brand-github.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/themes/stack/assets/icons/brand-twitter.svg b/themes/stack/assets/icons/brand-twitter.svg new file mode 100644 index 0000000..17ab1b1 --- /dev/null +++ b/themes/stack/assets/icons/brand-twitter.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/themes/stack/assets/icons/categories.svg b/themes/stack/assets/icons/categories.svg new file mode 100644 index 0000000..e00ab1d --- /dev/null +++ b/themes/stack/assets/icons/categories.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/themes/stack/assets/icons/chevron-left.svg b/themes/stack/assets/icons/chevron-left.svg new file mode 100644 index 0000000..4480a0a --- /dev/null +++ b/themes/stack/assets/icons/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/themes/stack/assets/icons/chevron-right.svg b/themes/stack/assets/icons/chevron-right.svg new file mode 100644 index 0000000..2be2e9c --- /dev/null +++ b/themes/stack/assets/icons/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/themes/stack/assets/icons/clock.svg b/themes/stack/assets/icons/clock.svg new file mode 100644 index 0000000..a7a8be6 --- /dev/null +++ b/themes/stack/assets/icons/clock.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/stack/assets/icons/copyright.svg b/themes/stack/assets/icons/copyright.svg new file mode 100644 index 0000000..2ac45ff --- /dev/null +++ b/themes/stack/assets/icons/copyright.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/stack/assets/icons/date.svg b/themes/stack/assets/icons/date.svg new file mode 100644 index 0000000..ed92a90 --- /dev/null +++ b/themes/stack/assets/icons/date.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/themes/stack/assets/icons/dots.svg b/themes/stack/assets/icons/dots.svg new file mode 100644 index 0000000..0e7be05 --- /dev/null +++ b/themes/stack/assets/icons/dots.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/themes/stack/assets/icons/hash.svg b/themes/stack/assets/icons/hash.svg new file mode 100644 index 0000000..e00ab1d --- /dev/null +++ b/themes/stack/assets/icons/hash.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/themes/stack/assets/icons/home.svg b/themes/stack/assets/icons/home.svg new file mode 100644 index 0000000..ed5fd53 --- /dev/null +++ b/themes/stack/assets/icons/home.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/themes/stack/assets/icons/infinity.svg b/themes/stack/assets/icons/infinity.svg new file mode 100644 index 0000000..fb40b24 --- /dev/null +++ b/themes/stack/assets/icons/infinity.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/themes/stack/assets/icons/language.svg b/themes/stack/assets/icons/language.svg new file mode 100644 index 0000000..66ede1c --- /dev/null +++ b/themes/stack/assets/icons/language.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/themes/stack/assets/icons/link.svg b/themes/stack/assets/icons/link.svg new file mode 100644 index 0000000..3c87803 --- /dev/null +++ b/themes/stack/assets/icons/link.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/stack/assets/icons/messages.svg b/themes/stack/assets/icons/messages.svg new file mode 100644 index 0000000..725d75c --- /dev/null +++ b/themes/stack/assets/icons/messages.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/stack/assets/icons/rss.svg b/themes/stack/assets/icons/rss.svg new file mode 100644 index 0000000..b92ea8f --- /dev/null +++ b/themes/stack/assets/icons/rss.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/themes/stack/assets/icons/search.svg b/themes/stack/assets/icons/search.svg new file mode 100644 index 0000000..a0b0ddc --- /dev/null +++ b/themes/stack/assets/icons/search.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/stack/assets/icons/tag.svg b/themes/stack/assets/icons/tag.svg new file mode 100644 index 0000000..ef7a63d --- /dev/null +++ b/themes/stack/assets/icons/tag.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/stack/assets/icons/toggle-left.svg b/themes/stack/assets/icons/toggle-left.svg new file mode 100644 index 0000000..c3048d4 --- /dev/null +++ b/themes/stack/assets/icons/toggle-left.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/stack/assets/icons/toggle-right.svg b/themes/stack/assets/icons/toggle-right.svg new file mode 100644 index 0000000..d3edd63 --- /dev/null +++ b/themes/stack/assets/icons/toggle-right.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/stack/assets/icons/user.svg b/themes/stack/assets/icons/user.svg new file mode 100644 index 0000000..9c92c87 --- /dev/null +++ b/themes/stack/assets/icons/user.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/stack/assets/scss/breakpoints.scss b/themes/stack/assets/scss/breakpoints.scss new file mode 100644 index 0000000..e9e9de7 --- /dev/null +++ b/themes/stack/assets/scss/breakpoints.scss @@ -0,0 +1,17 @@ +$breakpoints: ( + sm: 640px, + md: 768px, + lg: 1024px, + xl: 1280px, + 2xl: 1536px, +); + +@mixin respond($breakpoint) { + @if not map-has-key($breakpoints, $breakpoint) { + @warn "'#{$breakpoint}' is not a valid breakpoint"; + } @else { + @media (min-width: map-get($breakpoints, $breakpoint)) { + @content; + } + } +} diff --git a/themes/stack/assets/scss/custom.scss b/themes/stack/assets/scss/custom.scss new file mode 100644 index 0000000..61fa80f --- /dev/null +++ b/themes/stack/assets/scss/custom.scss @@ -0,0 +1 @@ +/* Place your custom SCSS in HUGO_SITE_FOLDER/assets/scss/custom.scss */ \ No newline at end of file diff --git a/themes/stack/assets/scss/external/normalize.scss b/themes/stack/assets/scss/external/normalize.scss new file mode 100644 index 0000000..c45a85f --- /dev/null +++ b/themes/stack/assets/scss/external/normalize.scss @@ -0,0 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + + html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + } + + /* Sections + ========================================================================== */ + + /** + * Remove the margin in all browsers. + */ + + body { + margin: 0; + } + + /** + * Render the `main` element consistently in IE. + */ + + main { + display: block; + } + + /** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + + h1 { + font-size: 2em; + margin: 0.67em 0; + } + + /* Grouping content + ========================================================================== */ + + /** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + + hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ + } + + /** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + + pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ + } + + /* Text-level semantics + ========================================================================== */ + + /** + * Remove the gray background on active links in IE 10. + */ + + a { + background-color: transparent; + } + + /** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + + abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ + } + + /** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + + b, + strong { + font-weight: bolder; + } + + /** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + + code, + kbd, + samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ + } + + /** + * Add the correct font size in all browsers. + */ + + small { + font-size: 80%; + } + + /** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + /* Embedded content + ========================================================================== */ + + /** + * Remove the border on images inside links in IE 10. + */ + + img { + border-style: none; + } + + /* Forms + ========================================================================== */ + + /** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + + button, + input, + optgroup, + select, + textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ + } + + /** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + + button, + input { /* 1 */ + overflow: visible; + } + + /** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + + button, + select { /* 1 */ + text-transform: none; + } + + /** + * Correct the inability to style clickable types in iOS and Safari. + */ + + button, + [type="button"], + [type="reset"], + [type="submit"] { + -webkit-appearance: button; + } + + /** + * Remove the inner border and padding in Firefox. + */ + + button::-moz-focus-inner, + [type="button"]::-moz-focus-inner, + [type="reset"]::-moz-focus-inner, + [type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; + } + + /** + * Restore the focus styles unset by the previous rule. + */ + + button:-moz-focusring, + [type="button"]:-moz-focusring, + [type="reset"]:-moz-focusring, + [type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; + } + + /** + * Correct the padding in Firefox. + */ + + fieldset { + padding: 0.35em 0.75em 0.625em; + } + + /** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + + legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ + } + + /** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + + progress { + vertical-align: baseline; + } + + /** + * Remove the default vertical scrollbar in IE 10+. + */ + + textarea { + overflow: auto; + } + + /** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + + [type="checkbox"], + [type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + } + + /** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + + [type="number"]::-webkit-inner-spin-button, + [type="number"]::-webkit-outer-spin-button { + height: auto; + } + + /** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + + [type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ + } + + /** + * Remove the inner padding in Chrome and Safari on macOS. + */ + + [type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + } + + /** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + + ::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ + } + + /* Interactive + ========================================================================== */ + + /* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + + details { + display: block; + } + + /* + * Add the correct display in all browsers. + */ + + summary { + display: list-item; + } + + /* Misc + ========================================================================== */ + + /** + * Add the correct display in IE 10+. + */ + + template { + display: none; + } + + /** + * Add the correct display in IE 10. + */ + + [hidden] { + display: none; + } \ No newline at end of file diff --git a/themes/stack/assets/scss/general.scss b/themes/stack/assets/scss/general.scss new file mode 100644 index 0000000..c362594 --- /dev/null +++ b/themes/stack/assets/scss/general.scss @@ -0,0 +1,31 @@ +a { + text-decoration: none; + color: var(--accent-color); + + &:hover { + color: var(--accent-color-darker); + } + + &.link { + box-shadow: 0px -2px 0px rgba(var(--link-background-color), var(--link-background-opacity)) inset; + transition: all 0.3s ease; + + &:hover { + box-shadow: 0px calc(-1rem * var(--article-line-height)) 0px rgba(var(--link-background-color), var(--link-background-opacity-hover)) inset; + } + } +} + +.section-title { + text-transform: uppercase; + margin-top: 0; + margin-bottom: var(--spacing-sm); + display: block; + font-size: 1.6rem; + font-weight: bold; + color: var(--body-text-color); + + a { + color: var(--body-text-color); + } +} diff --git a/themes/stack/assets/scss/grid.scss b/themes/stack/assets/scss/grid.scss new file mode 100644 index 0000000..a27e6f8 --- /dev/null +++ b/themes/stack/assets/scss/grid.scss @@ -0,0 +1,99 @@ +.container { + margin-left: auto; + margin-right: auto; + + .left-sidebar { + order: -3; + width: 100%; + max-width: var(--left-sidebar-max-width); + } + + .right-sidebar { + order: -1; + width: 100%; + max-width: var(--right-sidebar-max-width); + + /// Display right sidebar when min-width: lg + @include respond(lg) { + display: flex; + } + } + + &.extended { + @include respond(md) { + max-width: 1024px; + --left-sidebar-max-width: 25%; + --right-sidebar-max-width: 30%; + } + + @include respond(lg) { + max-width: 1280px; + --left-sidebar-max-width: 20%; + --right-sidebar-max-width: 30%; + } + + @include respond(xl) { + max-width: 1536px; + --left-sidebar-max-width: 15%; + --right-sidebar-max-width: 25%; + } + } + + &.compact { + @include respond(md) { + --left-sidebar-max-width: 25%; + max-width: 768px; + } + + @include respond(lg) { + max-width: 1024px; + --left-sidebar-max-width: 20%; + } + + @include respond(xl) { + max-width: 1280px; + } + } +} + +.flex { + display: flex; + flex-direction: row; + + &.on-phone--column { + flex-direction: column; + @include respond(md) { + flex-direction: row; + } + } + + .full-width { + width: 100%; + } +} + +main.main { + order: -2; + min-width: 0; + max-width: 100%; + flex-grow: 1; + display: flex; + flex-direction: column; + gap: var(--section-separation); + + @include respond(md) { + padding-top: var(--main-top-padding); + } +} + +.main-container { + min-height: 100vh; + align-items: flex-start; + padding: 0 var(--container-padding); + gap: var(--section-separation); + padding-top: var(--main-top-padding); + + @include respond(md) { + padding-top: 0; + } +} diff --git a/themes/stack/assets/scss/mixins.scss b/themes/stack/assets/scss/mixins.scss new file mode 100644 index 0000000..d905f66 --- /dev/null +++ b/themes/stack/assets/scss/mixins.scss @@ -0,0 +1,5 @@ +@mixin card-style { + background-color: var(--card-background); + box-shadow: var(--shadow-l1); + border-radius: var(--card-border-radius); +} diff --git a/themes/stack/assets/scss/partials/article.scss b/themes/stack/assets/scss/partials/article.scss new file mode 100644 index 0000000..6fc5462 --- /dev/null +++ b/themes/stack/assets/scss/partials/article.scss @@ -0,0 +1,400 @@ +/* Default article style */ +.article-list { + display: flex; + flex-direction: column; + gap: var(--section-separation); + + article { + display: flex; + flex-direction: column; + @include card-style; + overflow: hidden; + + transition: box-shadow 0.3s ease; + + &:hover { + box-shadow: var(--shadow-l2); + } + + .article-image { + img { + width: 100%; + height: var(--article-image-height); + object-fit: cover; + } + } + } +} + +.article-details { + display: flex; + flex-direction: column; + justify-content: center; + padding: var(--card-padding); + gap: var(--spacing-md); +} + +.article-title { + font-family: var(--article-font-family); + font-weight: 600; + margin: 0; + color: var(--card-text-color-main); + font-size: 2.2rem; + + @include respond(xl) { + font-size: 2.4rem; + } + + a { + color: var(--card-text-color-main); + + &:hover { + opacity: 0.8; + } + } +} + +.article-subtitle { + font-weight: normal; + color: var(--card-text-color-secondary); + line-height: 1.5; + margin: 0; + font-size: 1.75rem; + @include respond(xl) { + font-size: 2rem; + } +} + +.article-title-wrapper { + display: flex; + flex-direction: column; + gap: 8px; +} + +.article-meta { + display: flex; + flex-direction: column; + gap: var(--spacing-lg); + font-size: 1.4rem; + + svg { + vertical-align: middle; + width: 20px; + height: 20px; + stroke-width: 1.33; + flex-shrink: 0; + } + + &, + time, + a, + span { + font-size: 1.4rem; + color: var(--card-text-color-tertiary); + } + + & > .inline-meta { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-md); + } +} + +.article-category, +.article-tags { + display: flex; + gap: var(--spacing-sm); + flex-wrap: wrap; + + a { + color: var(--accent-color-text); + background-color: var(--accent-color); + padding: 8px 16px; + border-radius: var(--tag-border-radius); + display: inline-block; + font-size: 1.4rem; + transition: background-color 0.5s ease; + + &:hover { + color: var(--accent-color-text); + background-color: var(--accent-color-darker); + } + } +} + +/* Compact style article list */ +.article-list--compact { + @include card-style; + --image-size: var(--article-compact-image-size); + + article { + & > a { + display: flex; + align-items: center; + padding: var(--small-card-padding); + gap: var(--spacing-md); + } + + &:not(:last-of-type) { + border-bottom: 1.5px solid var(--card-separator-color); + } + + .article-details { + flex-grow: 1; + padding: 0; + min-height: var(--image-size); + gap: var(--spacing-sm); + } + + .article-title { + margin: 0; + font-size: 1.6rem; + + @include respond(md) { + font-size: 1.8rem; + } + } + + .article-image { + img { + width: var(--image-size); + height: var(--image-size); + object-fit: cover; + } + } + + .article-preview { + font-size: 1.4rem; + color: var(--card-text-color-tertiary); + line-height: 1.5; + } + } +} + +/* Tile style article list */ +.article-list--tile { + article { + position: relative; + width: var(--article-tile-width); + height: var(--article-tile-height); + @include card-style; + overflow: hidden; + transition: box-shadow 0.3s ease; + + &:hover { + box-shadow: var(--shadow-l2); + } + + &.has-image { + .article-details { + background-color: var(--article-tile-overlay-bg); + } + + .article-title { + color: var(--article-tile-text-color); + } + } + + .article-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .article-details { + border-radius: var(--card-border-radius); + position: relative; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + z-index: 2; + padding: 15px; + + @include respond(sm) { + padding: 20px; + } + } + + .article-title { + font-size: 2rem; + font-weight: 500; + color: var(--card-text-color-main); + + @include respond(sm) { + font-size: 2.2rem; + } + } + } +} + +.mermaid:not([data-processed]) { + background: var(--card-background); + min-height: 150px; + border-radius: 8px; + position: relative; + overflow: hidden; + font-size: 0 !important; + line-height: 0; + color: transparent; + + &::after { + visibility: visible; + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 28px; + height: 28px; + margin: -14px 0 0 -14px; + border: 3px solid var(--card-background); + border-top-color: var(--accent-color, #3273dc); + border-radius: 50%; + animation: mermaid-spinner 0.8s linear infinite; + } +} + +@keyframes mermaid-spinner { + to { + transform: rotate(360deg); + } +} + +// Mermaid diagram wrapper with expand toolbar +.mermaid-wrapper { + position: relative; + margin: 1rem 0; + + .mermaid { + display: flex; + justify-content: center; + + svg { + display: block; + max-width: 100%; + height: auto; + } + } +} + +.mermaid-toolbar { + position: absolute; + top: 0.75rem; + right: 0.75rem; + display: flex; + gap: 0.5rem; + z-index: 10; + + button { + padding: 0.5rem 1rem; + background: var(--mermaid-toolbar-btn-bg); + border: 2px solid var(--mermaid-toolbar-btn-text); + border-radius: 6px; + cursor: pointer; + font-size: 1rem; + font-weight: 600; + color: var(--mermaid-toolbar-btn-text); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + transition: all 0.2s; + + &:hover { + background: var(--mermaid-toolbar-btn-text); + color: var(--card-background); + transform: scale(1.05); + } + } +} + +// Mermaid fullscreen modal +.mermaid-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: var(--mermaid-modal-bg); + z-index: 9999; + + &.active { + display: flex; + flex-direction: column; + } +} + +.mermaid-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background: rgba(255, 255, 255, 0.1); +} + +.mermaid-modal-controls { + display: flex; + gap: 0.5rem; +} + +.mermaid-modal-controls button, +.mermaid-modal-close { + padding: 0.5rem 1rem; + background: var(--mermaid-modal-btn-bg); + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + color: var(--card-text-color-main); + + &:hover { + background: var(--mermaid-modal-btn-hover); + } +} + +.mermaid-modal-body { + flex: 1; + overflow: hidden; + position: relative; +} + +.mermaid-modal-content { + position: absolute; + top: 1rem; + left: 1rem; + right: 1rem; + bottom: 1rem; + border-radius: 8px; + overflow: hidden; + background: var(--mermaid-modal-content-bg); +} + +.mermaid-panzoom-container { + display: inline-block; + transform-origin: 0 0; + visibility: hidden; + + &.ready { + visibility: visible; + } + + svg { + display: block; + } +} + +// Off-screen container for pre-rendering alternate theme +.mermaid-offscreen { + position: absolute; + left: -9999px; + visibility: hidden; + width: 800px; +} diff --git a/themes/stack/assets/scss/partials/base.scss b/themes/stack/assets/scss/partials/base.scss new file mode 100644 index 0000000..bf345f8 --- /dev/null +++ b/themes/stack/assets/scss/partials/base.scss @@ -0,0 +1,21 @@ +html { + font-size: 62.5%; + overflow-y: scroll; + scrollbar-gutter: stable; +} + +* { + box-sizing: border-box; +} + +body { + background: var(--body-background); + margin: 0; + font-family: var(--base-font-family); + font-size: 1.6rem; + text-autospace: ideograph-alpha ideograph-numeric punctuation insert; + text-spacing-trim: trim-start allow-end; + -ms-text-autospace: ideograph-alpha; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} \ No newline at end of file diff --git a/themes/stack/assets/scss/partials/comments/artalk.scss b/themes/stack/assets/scss/partials/comments/artalk.scss new file mode 100644 index 0000000..4d8da6a --- /dev/null +++ b/themes/stack/assets/scss/partials/comments/artalk.scss @@ -0,0 +1,80 @@ +@import "../../breakpoints"; +@import "../../mixins"; + +.artalk-container { + @include card-style; + padding: 5px; + + @include respond(md) { + padding: var(--card-padding); + } +} + +.artalk { + --at-color-bg: var(--card-background); + --at-color-font: var(--card-text-color-main); + --at-color-sub: var(--card-text-color-secondary); + --at-color-main: var(--accent-color); + --at-color-border: var(--card-separator-color); + --at-border-radius: var(--card-border-radius); + --at-color-link: var(--accent-color); +} + +.artalk a { + color: var(--at-color-link); + text-decoration: none; + transition: color 0.3s ease; + + &:hover { + color: var(--accent-color-darker); + } +} + +.artalk .atk-content a, +.artalk .atk-editor-plug-preview a { + color: var(--accent-color); + box-shadow: 0px -2px 0px rgba(var(--link-background-color), var(--link-background-opacity)) inset; + transition: all 0.3s ease; + + &:hover { + color: var(--accent-color-darker); + box-shadow: 0px calc(-1rem * var(--article-line-height)) 0px rgba(var(--link-background-color), var(--link-background-opacity-hover)) inset; + } +} + +[data-scheme="dark"] .artalk { + --at-color-main: var(--accent-color); + --at-color-link: var(--accent-color); + --at-color-bg: var(--card-background); + --at-color-bg-transl: rgba(66, 66, 66, 0.95); + --at-color-bg-grey: rgba(255, 255, 255, 0.08); + --at-color-bg-grey-transl: rgba(255, 255, 255, 0.06); + --at-color-bg-light: rgba(255, 255, 255, 0.08); + --at-color-border: var(--card-separator-color); + --at-color-font: var(--card-text-color-main); + --at-color-deep: var(--card-text-color-main); + --at-color-sub: var(--card-text-color-secondary); + --at-color-meta: var(--card-text-color-secondary); + --at-color-grey: var(--card-text-color-secondary); + --at-color-gradient: linear-gradient( + 180deg, + transparent, + var(--card-background) + ); +} + +.artalk code { + font-family: var(--code-font-family); +} + +.artalk .atk-avatar img { + border-radius: var(--card-border-radius); +} + +[data-scheme="dark"] .artalk .atk-avatar img { + filter: brightness(0.75); +} + +[data-scheme="dark"] .artalk .atk-send-btn { + color: var(--accent-color-text); +} diff --git a/themes/stack/assets/scss/partials/comments/disqusjs.scss b/themes/stack/assets/scss/partials/comments/disqusjs.scss new file mode 100644 index 0000000..eb270e3 --- /dev/null +++ b/themes/stack/assets/scss/partials/comments/disqusjs.scss @@ -0,0 +1,394 @@ +.disqus-container { + background-color: var(--card-background); + border-radius: var(--card-border-radius); + box-shadow: var(--shadow-l1); + padding: var(--card-padding); +} + +#dsqjs * { + margin: 0; + padding: 0 +} + +#dsqjs a { + text-decoration: none; + color: #076dd0 +} + +#dsqjs .dsqjs-hide { + display: none!important +} + +#dsqjs .dsqjs-disabled { + cursor: not-allowed; + opacity: .5 +} + +#dsqjs #dsqjs-msg { + text-align: center; + margin-top: 4px; + margin-bottom: 4px; + font-size: 14px +} + +#dsqjs #dsqjs-msg .dsqjs-msg-btn { + cursor: pointer +} + +#dsqjs .dsqjs-bullet { + line-height: 1.4; + margin: 0 2px +} + +#dsqjs .dsqjs-bullet:after { + color: #c2c6cc; + content: "·"; + font-weight: 700 +} + +#dsqjs .dsqjs-clearfix:after,#dsqjs .dsqjs-clearfix:before { + display: table; + content: ""; + line-height: 0; + clear: both +} + +#dsqjs .dsqjs-nav { + position: relative; + margin: 0 0 20px; + border-bottom: 2px solid #e7e9ee +} + +#dsqjs ol,#dsqjs ul { + list-style: none; + list-style-type: none +} + +#dsqjs .dsqjs-no-comment { + text-align: center; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; + overflow: hidden; + color: #2a2e2e; + margin-bottom: 6px +} + +#dsqjs .dsqjs-nav-tab { + float: left; + text-transform: capitalize; + font-size: 15px; + padding: 12px 8px; + color: #656c7a; + display: block; + margin: 0 15px 0 0; + font-weight: 700; + line-height: 1; + position: relative; + transition: all .2s ease-in-out +} + +#dsqjs .dsqjs-nav-tab:last-child { + margin: 0 +} + +#dsqjs .dsqjs-tab-active { + color: #2a2e2e +} + +#dsqjs .dsqjs-tab-active>span:after { + content: " "; + display: block; + height: 2px; + background-color: #076dd0!important; + position: absolute; + bottom: -5px; + left: 0; + right: 0 +} + +#dsqjs .dsqjs-post-list .dsqjs-post-item { + position: relative; + margin-bottom: 16px +} + +#dsqjs .dsqjs-post-list .dsqjs-post-avatar { + float: left; + margin-right: 10px; + position: relative; + background: #dbdfe4; + padding: 0; + display: block; + border-radius: 4px +} + +#dsqjs .dsqjs-post-list .dsqjs-post-avatar img { + width: 44px; + height: 44px; + display: block; + border-radius: 4px +} + +#dsqjs .dsqjs-post-list .dsqjs-post-header { + line-height: 1; + font-size: 14px; + margin-bottom: 3px +} + +#dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-post-author { + color: #656c7a; + font-weight: 700 +} + +#dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-admin-badge { + color: #fff; + background: #687a86; + padding: 1px 3px; + margin-left: 4px; + font-size: 12px; + line-height: 1; + font-weight: 700; + border-radius: 3px; + display: inline-block; + position: relative; + top: -1px; + left: 1px +} + +#dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-meta { + display: inline-block; + font-size: 12px; + color: #656c7a +} + +#dsqjs .dsqjs-post-body { + font-size: 15px; + line-height: 1.5; + word-wrap: break-word; + overflow: hidden; + color: #2a2e2e +} + +#dsqjs .dsqjs-post-body code { + padding: .2em .4em; + margin: 0; + font-size: 85%; + background: #f5f5f5; + color: inherit; + border-radius: 3px +} + +#dsqjs .dsqjs-post-body pre { + padding: .5em; + overflow: auto; + font-size: 85%; + line-height: 1.45; + border-radius: 3px; + background: #f5f5f5; + margin: .5em 0 +} + +#dsqjs .dsqjs-post-body blockquote { + padding: 0 .8em; + margin: .5em 0; + color: #6a737d; + border-left: .25em solid #dfe2e5 +} + +#dsqjs .dsqjs-post-body p:last-child { + margin: 0 +} + +#dsqjs .dsqjs-post-list.dsqjs-children>li { + margin-left: 30px +} + +#dsqjs .dsqjs-post-list.dsqjs-children .dsqjs-post-avatar img { + width: 38px; + height: 38px +} + +#dsqjs .dsqjs-load-more { + font-size: 14px; + font-weight: 400; + display: block; + text-align: center; + padding: 11px 14px; + margin: 0 0 24px; + background: #687a86; + color: #fff; + cursor: pointer +} + +#dsqjs .dsqjs-load-more:hover { + opacity: .8 +} + +#dsqjs footer { + text-align: right; + line-height: 1.5; + padding-top: 10px; + padding-right: 10px; + border-top: 2px solid #e7e9ee; + margin-top: 12px; + font-weight: 700; + font-size: 16px; + color: #555 +} + +#dsqjs .dsqjs-disqus-logo { + background-image: url(https://c.disquscdn.com/next/embed/assets/img/sprite.654110a9206fd22f08cca0798e34a65e.png); + background-repeat: no-repeat; + display: inline-block; + background-size: 86px 40.5px; + height: 16.5px; + width: 86px; +} + +#dsqjs .dsqjs-order { + display: flex; + float: right; + align-items: center; + margin-top: 10px; + margin-bottom: 12px +} + +#dsqjs .dsqjs-order-radio { + display: none +} + +#dsqjs .dsqjs-order-radio:checked+.dsqjs-order-label { + color: #fff; + background-color: #888 +} + +#dsqjs .dsqjs-order-label { + display: block; + height: 20px; + line-height: 20px; + margin-right: 10px; + font-size: 12px; + border-radius: 2px; + padding: 0 5px; + background-color: #dcdcdc; + cursor: pointer +} + +#dsqjs p.dsqjs-has-more { + margin-bottom: 24px; + margin-left: 48px; + font-size: 13px; + line-height: 15px +} + +#dsqjs p.dsqjs-has-more a.dsqjs-has-more-btn { + color: #656c7a; + text-decoration: underline; + cursor: pointer +} + +@media (min-width: 768px) { + #dsqjs .dsqjs-post-list.dsqjs-children>li { + margin-left:48px + } + + #dsqjs .dsqjs-post-list .dsqjs-post-avatar { + margin-right: 12px + } + + #dsqjs .dsqjs-post-list .dsqjs-post-item { + margin-bottom: 20px + } +} + +@media (min-width: 1024px) { + #dsqjs .dsqjs-post-list.dsqjs-children>li { + margin-left:60px + } +} + +:root[data-scheme="light"] { + #dsqjs .dsqjs-disqus-logo { + background-position: 0 -7px; + } +} + +:root[data-scheme="dark"] { + #dsqjs { + --t-s: rgba(255,255,255,0.9); + --alt: #3e4b5e; + --link-hover: #47a2e0; + --hover-bg: #3e4b5e; + --tag: #3e4b5e; + --border: #435266; + --pre: #3c495b; + --c-bg: #2f3947; + --code: #c3c7cb; + --kbd: #4e5f77; + --hl: #abb2bf; + --hlc: #808895; + --hlk: #c678dd; + --hln: #e06c75; + --hll: #56b6c2; + --hls: #98c379; + --hlt: #e6c07b; + --hlv: #d19a66; + --bg: #181c27; + --main: #252d38; + --t: rgba(255,255,255,0.86); + --t-l: rgba(255,255,255,0.66); + --logo: #fff; + --link: #38a3fd; + --title: rgba(255,255,255,0.92); + --fab: #364151; + --shadow: none; + } + + #disqus_thread { + color: var(--body-text-color) + } + + #dsqjs #dsqjs-msg { + color: var(--t) + } + + #dsqjs a { + color:var(--link) + } + + #dsqjs a:focus,#dsqjs a:hover { + color: var(--link-hover) + } + + #dsqjs .dsqjs-disqus-logo { + background-position: 0 -24px; + } + + #dsqjs .dsqjs-nav,#dsqjs footer { + border-color: var(--hlc) + } + + #dsqjs .dsqjs-load-more,#dsqjs .dsqjs-load-more:hover,#dsqjs .dsqjs-nav-tab,#dsqjs .dsqjs-no-comment,#dsqjs .dsqjs-post-content { + color: var(--t) + } + + #dsqjs .dsqjs-order-label { + background-color: var(--hlc) + } + + #dsqjs .dsqjs-order-radio:checked+.dsqjs-order-label { + background-color: var(--kbd) + } + + #dsqjs .dsqjs-tab-active>span:after { + background-color: #2e9fff + } + + #dsqjs .dsqjs-footer,#dsqjs .dsqjs-meta { + color: var(--t-l) + } + + #dsqjs .dsqjs-post-body blockquote { + border-color: var(--border) + } +} diff --git a/themes/stack/assets/scss/partials/cookies.scss b/themes/stack/assets/scss/partials/cookies.scss new file mode 100644 index 0000000..67dc82f --- /dev/null +++ b/themes/stack/assets/scss/partials/cookies.scss @@ -0,0 +1,199 @@ +.cookie-banner { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 9999; + padding: var(--container-padding); + pointer-events: none; + + &[aria-hidden="true"] { + display: none; + } +} + +.cookie-banner__content { + max-width: 900px; + margin: 0 auto; + background: var(--card-background); + border-radius: var(--card-border-radius); + box-shadow: var(--shadow-l2); + padding: var(--card-padding); + display: flex; + flex-direction: column; + gap: var(--spacing-md); + pointer-events: auto; + + @include respond(md) { + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-lg); + } +} + +.cookie-banner__text { + flex: 1; + min-width: 200px; + + strong { + display: block; + color: var(--card-text-color-main); + font-size: 1.6rem; + margin-bottom: var(--spacing-xs); + } + + p { + color: var(--card-text-color-secondary); + font-size: 1.4rem; + line-height: 1.6; + margin: 0; + } +} + +.cookie-banner__actions { + display: flex; + gap: var(--spacing-sm); + flex-shrink: 0; +} + +.cookie-banner__settings-link { + width: auto; + align-self: flex-start; + background: none; + border: none; + color: var(--accent-color); + font-size: 1.3rem; + cursor: pointer; + padding: 5px; + + &:hover { + text-decoration: underline; + } + + @include respond(md) { + margin-left: auto; + align-self: center; + } +} + +.cookie-btn { + padding: 10px 20px; + border-radius: var(--tag-border-radius); + font-size: 1.4rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; + + &--primary { + background: var(--accent-color); + color: var(--accent-color-text); + + &:hover { + background: var(--accent-color-darker); + } + } + + &--secondary { + background: transparent; + color: var(--card-text-color-main); + border: 1.5px solid var(--card-separator-color); + + &:hover { + background: var(--card-background-selected); + } + } +} + +// Cookie settings panel +.cookie-settings { + padding-top: 20px; + border-top: 1.5px solid var(--card-separator-color); + + &[aria-hidden="true"] { + display: none; + } + + h3 { + color: var(--card-text-color-main); + font-size: 1.6rem; + margin: 0 0 var(--spacing-md) 0; + } + + &__actions { + display: flex; + justify-content: flex-end; + gap: var(--spacing-sm); + margin-top: var(--spacing-lg); + } +} + +.cookie-category { + padding: var(--spacing-md) 0; + border-bottom: 1px solid var(--card-separator-color); + + &:last-of-type { + border-bottom: none; + } + + label { + display: flex; + align-items: center; + gap: var(--spacing-sm); + cursor: pointer; + + input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--accent-color); + cursor: pointer; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } + + strong { + color: var(--card-text-color-main); + font-size: 1.5rem; + } + } + + p { + color: var(--card-text-color-secondary); + font-size: 1.3rem; + margin: 8px 0 0 28px; + line-height: 1.5; + } +} + +// Footer link for reopening settings +.cookie-settings-link { + display: inline-block; + color: var(--body-text-color); + font-size: 1.2rem; + cursor: pointer; + background: none; + border: none; + padding: 0; + + &:hover { + color: var(--accent-color); + text-decoration: underline; + } +} + +// Comments placeholder when consent not given +.consent-placeholder { + @include card-style; + padding: var(--card-padding); + text-align: center; + color: var(--card-text-color-secondary); + font-size: 1.4rem; + + p { + margin: 0 0 var(--spacing-md) 0; + } +} \ No newline at end of file diff --git a/themes/stack/assets/scss/partials/footer.scss b/themes/stack/assets/scss/partials/footer.scss new file mode 100644 index 0000000..4d1b9c7 --- /dev/null +++ b/themes/stack/assets/scss/partials/footer.scss @@ -0,0 +1,30 @@ +footer.site-footer { + padding: var(--spacing-lg) 0 var(--section-separation) 0; + font-size: 1.4rem; + line-height: 1.75; + + &:before { + content: ""; + display: block; + height: 3px; + width: 50px; + background: var(--body-text-color); + margin-bottom: var(--spacing-lg); + } + + .copyright { + color: var(--accent-color); + font-weight: bold; + margin-bottom: var(--spacing-xs); + } + + .powerby { + color: var(--body-text-color); + font-weight: normal; + font-size: 1.2rem; + + a { + color: var(--body-text-color); + } + } +} diff --git a/themes/stack/assets/scss/partials/highlight/common.scss b/themes/stack/assets/scss/partials/highlight/common.scss new file mode 100644 index 0000000..e2fabbf --- /dev/null +++ b/themes/stack/assets/scss/partials/highlight/common.scss @@ -0,0 +1,434 @@ +/* Background */ +.chroma { + color: $color; + background-color: $background-color; +} + +/* Other */ +.chroma .x { +} + +/* Error */ +.chroma .err { + color: $error-color; +} + +/* LineTableTD */ +.chroma .lntd { + vertical-align: top; + padding: 0; + margin: 0; + border: 0; +} + +/* LineTable */ +.chroma .lntable { + border-spacing: 0; + padding: 0; + margin: 0; + border: 0; + width: 100%; + display: block; + + > tbody { + display: block; + width: 100%; + > tr { + display: flex; + width: 100%; + > td:last-child { + overflow-x: auto; + } + } + } +} + +/* LineHighlight */ +.chroma .hl { + display: block; + width: 100%; + background-color: #ffffcc; +} + +/* LineNumbersTable */ +.chroma .lnt { + margin-right: 0; + user-select: none; + padding: 0 0.4em 0 0.4em; + color: #7f7f7f; + display: block; +} + +/* Optimise code block selection experience */ +span.line { + margin-left: 0.4em; +} + +/* LineNumbers */ +.chroma .ln { + margin-right: 0.4em; + padding: 0 0.4em 0 0.4em; + color: #7f7f7f; +} + +/* Keyword */ +.chroma .k { + color: $keyword-color; +} + +/* KeywordConstant */ +.chroma .kc { + color: $keyword-color; +} + +/* KeywordDeclaration */ +.chroma .kd { + color: $keyword-color; +} + +/* KeywordNamespace */ +.chroma .kn { + color: #f92672; +} + +/* KeywordPseudo */ +.chroma .kp { + color: $keyword-color; +} + +/* KeywordReserved */ +.chroma .kr { + color: $keyword-color; +} + +/* KeywordType */ +.chroma .kt { + color: $keyword-color; +} + +/* Name */ +.chroma .n { + color: $text-color; +} + +/* NameAttribute */ +.chroma .na { + color: $name-color; +} + +/* NameBuiltin */ +.chroma .nb { + color: $text-color; +} + +/* NameBuiltinPseudo */ +.chroma .bp { + color: $text-color; +} + +/* NameClass */ +.chroma .nc { + color: $name-color; +} + +/* NameConstant */ +.chroma .no { + color: $keyword-color; +} + +/* NameDecorator */ +.chroma .nd { + color: $name-color; +} + +/* NameEntity */ +.chroma .ni { + color: $text-color; +} + +/* NameException */ +.chroma .ne { + color: $name-color; +} + +/* NameFunction */ +.chroma .nf { + color: $name-color; +} + +/* NameFunctionMagic */ +.chroma .fm { + color: $text-color; +} + +/* NameLabel */ +.chroma .nl { + color: $text-color; +} + +/* NameNamespace */ +.chroma .nn { + color: $text-color; +} + +/* NameOther */ +.chroma .nx { + color: $name-color; +} + +/* NameProperty */ +.chroma .py { + color: $text-color; +} + +/* NameTag */ +.chroma .nt { + color: #f92672; +} + +/* NameVariable */ +.chroma .nv { + color: $text-color; +} + +/* NameVariableClass */ +.chroma .vc { + color: $text-color; +} + +/* NameVariableGlobal */ +.chroma .vg { + color: $text-color; +} + +/* NameVariableInstance */ +.chroma .vi { + color: $text-color; +} + +/* NameVariableMagic */ +.chroma .vm { + color: $text-color; +} + +/* Literal */ +.chroma .l { + color: #ae81ff; +} + +/* LiteralDate */ +.chroma .ld { + color: $literal-color; +} + +/* LiteralString */ +.chroma .s { + color: $literal-color; +} + +/* LiteralStringAffix */ +.chroma .sa { + color: $literal-color; +} + +/* LiteralStringBacktick */ +.chroma .sb { + color: $literal-color; +} + +/* LiteralStringChar */ +.chroma .sc { + color: $literal-color; +} + +/* LiteralStringDelimiter */ +.chroma .dl { + color: $literal-color; +} + +/* LiteralStringDoc */ +.chroma .sd { + color: $literal-color; +} + +/* LiteralStringDouble */ +.chroma .s2 { + color: $literal-color; +} + +/* LiteralStringEscape */ +.chroma .se { + color: #ae81ff; +} + +/* LiteralStringHeredoc */ +.chroma .sh { + color: $literal-color; +} + +/* LiteralStringInterpol */ +.chroma .si { + color: $literal-color; +} + +/* LiteralStringOther */ +.chroma .sx { + color: $literal-color; +} + +/* LiteralStringRegex */ +.chroma .sr { + color: $literal-color; +} + +/* LiteralStringSingle */ +.chroma .s1 { + color: $literal-color; +} + +/* LiteralStringSymbol */ +.chroma .ss { + color: $literal-color; +} + +/* LiteralNumber */ +.chroma .m { + color: #ae81ff; +} + +/* LiteralNumberBin */ +.chroma .mb { + color: #ae81ff; +} + +/* LiteralNumberFloat */ +.chroma .mf { + color: #ae81ff; +} + +/* LiteralNumberHex */ +.chroma .mh { + color: #ae81ff; +} + +/* LiteralNumberInteger */ +.chroma .mi { + color: #ae81ff; +} + +/* LiteralNumberIntegerLong */ +.chroma .il { + color: #ae81ff; +} + +/* LiteralNumberOct */ +.chroma .mo { + color: #ae81ff; +} + +/* Operator */ +.chroma .o { + color: #f92672; +} + +/* OperatorWord */ +.chroma .ow { + color: #f92672; +} + +/* Punctuation */ +.chroma .p { + color: $text-color; +} + +/* Comment */ +.chroma .c { + color: #75715e; +} + +/* CommentHashbang */ +.chroma .ch { + color: #75715e; +} + +/* CommentMultiline */ +.chroma .cm { + color: #75715e; +} + +/* CommentSingle */ +.chroma .c1 { + color: #75715e; +} + +/* CommentSpecial */ +.chroma .cs { + color: #75715e; +} + +/* CommentPreproc */ +.chroma .cp { + color: #75715e; +} + +/* CommentPreprocFile */ +.chroma .cpf { + color: #75715e; +} + +/* Generic */ +.chroma .g { +} + +/* GenericDeleted */ +.chroma .gd { + color: #f92672; +} + +/* GenericEmph */ +.chroma .ge { + font-style: italic; +} + +/* GenericError */ +.chroma .gr { +} + +/* GenericHeading */ +.chroma .gh { +} + +/* GenericInserted */ +.chroma .gi { + color: $name-color; +} + +/* GenericOutput */ +.chroma .go { +} + +/* GenericPrompt */ +.chroma .gp { +} + +/* GenericStrong */ +.chroma .gs { + font-weight: bold; +} + +/* GenericSubheading */ +.chroma .gu { + color: #75715e; +} + +/* GenericTraceback */ +.chroma .gt { +} + +/* GenericUnderline */ +.chroma .gl { +} + +/* TextWhitespace */ +.chroma .w { +} diff --git a/themes/stack/assets/scss/partials/highlight/dark.scss b/themes/stack/assets/scss/partials/highlight/dark.scss new file mode 100644 index 0000000..0d3f330 --- /dev/null +++ b/themes/stack/assets/scss/partials/highlight/dark.scss @@ -0,0 +1,14 @@ +/* +* Style: monokai +* https://xyproto.github.io/splash/docs/monokai.html +*/ + +$color: #f8f8f2; +$background-color: #272822; +$error-color: #bb0064; +$keyword-color: #66d9ef; +$text-color: $color; +$name-color: #a6e22e; +$literal-color: #e6db74; + +@import "common.scss"; diff --git a/themes/stack/assets/scss/partials/highlight/light.scss b/themes/stack/assets/scss/partials/highlight/light.scss new file mode 100644 index 0000000..174b649 --- /dev/null +++ b/themes/stack/assets/scss/partials/highlight/light.scss @@ -0,0 +1,14 @@ +/* +* Style: monokailight +* https://xyproto.github.io/splash/docs/monokailight.html +*/ + +$color: #272822; +$background-color: #fafafa; +$error-color: #960050; +$keyword-color: #00a8c8; +$text-color: #111111; +$name-color: #75af00; +$literal-color: #d88200; + +@import "common.scss"; diff --git a/themes/stack/assets/scss/partials/layout/404.scss b/themes/stack/assets/scss/partials/layout/404.scss new file mode 100644 index 0000000..d9d8752 --- /dev/null +++ b/themes/stack/assets/scss/partials/layout/404.scss @@ -0,0 +1,6 @@ +.not-found-card { + background-color: var(--card-background); + box-shadow: var(--shadow-l1); + border-radius: var(--card-border-radius); + padding: var(--card-padding); +} diff --git a/themes/stack/assets/scss/partials/layout/article.scss b/themes/stack/assets/scss/partials/layout/article.scss new file mode 100644 index 0000000..54607ab --- /dev/null +++ b/themes/stack/assets/scss/partials/layout/article.scss @@ -0,0 +1,486 @@ +.article-page { + &.hide-sidebar-sm .left-sidebar { + display: none; + + @include respond(md) { + display: inherit; + } + } + + .main-article { + background: var(--card-background); + border-radius: var(--card-border-radius); + box-shadow: var(--shadow-l1); + overflow: hidden; + + .article-header { + .article-image { + img { + height: auto; + width: 100%; + max-height: 50vh; + object-fit: cover; + } + } + + .article-details { + padding: var(--card-padding); + padding-bottom: 0; + } + } + + .article-content { + margin: var(--card-padding) 0; + color: var(--card-text-color-main); + + .footnotes { + font-family: var(--base-font-family); + } + + img { + max-width: 100%; + height: auto; + } + } + + .article-footer { + margin: var(--card-padding); + gap: var(--spacing-2xl); + } + } +} + +.toc-nav { + overflow-x: auto; + max-height: 75vh; + + ol, + ul { + margin: 0; + padding: 0; + } + + ol { + list-style-type: none; + counter-reset: item; + + li a:first-of-type::before { + counter-increment: item; + content: counters(item, ".") ". "; + font-weight: bold; + margin-right: 5px; + } + } + + &>ul { + padding: 0 1em; + } + + li { + margin: var(--spacing-md) 0 var(--spacing-md) var(--spacing-lg); + padding: var(--spacing-xs); + + &>ol, + &>ul { + margin-top: var(--spacing-sm); + padding-left: var(--spacing-sm); + margin-bottom: calc(var(--spacing-xs) * -1); + + &>li:last-child { + margin-bottom: 0; + } + } + } + + li.active-class>a { + border-left: var(--heading-border-size) solid var(--accent-color); + font-weight: bold; + } + + ul li.active-class>a { + display: block; + } + + @function repeat($str, $n) { + $result: ""; + + @for $_ from 0 to $n { + $result: $result + $str; + } + + @return $result; + } + + // Support up to 6 levels of indentation for lists in ToCs + @for $i from 0 to 5 { + &>ul #{repeat("> li > ul", $i)}>li.active-class>a { + $n: 25 + $i * 35; + margin-left: calc(-#{$n}px - 1em); + padding-left: calc(#{$n}px + 1em - var(--heading-border-size)); + } + + &>ol #{repeat("> li > ol", $i)}>li.active-class>a { + $n: 9 + $i * 35; + margin-left: calc(-#{$n}px - 1em); + padding-left: calc(#{$n}px + 1em - var(--heading-border-size)); + display: block; + } + } +} + +.widget--toc, +.article-toc { + display: flex; + flex-direction: column; + color: var(--card-text-color-main); +} + +.widget--toc { + background-color: var(--card-background); + border-radius: var(--card-border-radius); + box-shadow: var(--shadow-l1); + overflow: hidden; +} + +.article-toc { + padding: 0 var(--card-padding); + margin-top: var(--spacing-lg); + + @include respond(lg) { + display: none; + } + + details { + background-color: var(--card-background); + border-radius: var(--card-border-radius); + box-shadow: var(--shadow-l2); + + &[open] { + .article-toc-title { + svg { + transform: rotate(90deg); + } + } + } + + summary { + list-style: none; + padding: var(--spacing-md) var(--spacing-lg); + cursor: pointer; + + &::-webkit-details-marker { + display: none; + } + + .article-toc-title { + display: flex; + align-items: center; + gap: var(--spacing-sm); + text-transform: uppercase; + font-size: 1.4rem; + font-weight: bold; + color: var(--card-text-color-tertiary); + + svg { + width: 20px; + height: 20px; + stroke-width: 1.33; + transition: transform 0.3s ease; + } + } + } + } +} + +.article-content { + font-family: var(--article-font-family); + font-size: var(--article-font-size); + padding: 0 var(--card-padding); + line-height: var(--article-line-height); + + &>p { + margin: 1.5em 0; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + margin-inline-start: calc((var(--card-padding)) * -1); + padding-inline-start: calc(var(--card-padding) - var(--heading-border-size)); + border-inline-start: var(--heading-border-size) solid var(--accent-color); + position: relative; + + a.header-anchor { + transition: opacity 0.3s ease; + opacity: 0; + position: absolute; + left: 0; + width: var(--card-padding); + text-align: center; + color: var(--accent-color); + + &:before { + content: "#"; + } + } + + &:hover, + &:focus { + a.header-anchor { + opacity: 1; + } + } + } + + figure { + text-align: center; + + figcaption { + font-size: 1.4rem; + color: var(--card-text-color-secondary); + } + } + + blockquote { + position: relative; + margin: 1.5em 0; + border-inline-start: var(--blockquote-border-size) solid var(--card-separator-color); + padding: var(--spacing-md) calc(var(--card-padding) - var(--blockquote-border-size)); + background-color: var(--blockquote-background-color); + + .cite { + display: block; + text-align: right; + font-size: 0.75em; + + a { + text-decoration: underline; + } + } + } + + blockquote.alert { + .alert-header { + display: flex; + align-items: center; + gap: 0.5em; + font-weight: bold; + } + + .alert-body { + > :last-child { + margin-bottom: 0; + } + } + + $alert-types: note, tip, important, warning, caution; + + @each $type in $alert-types { + &.alert-#{$type} { + border-color: var(--alert-#{$type}-color); + background-color: var(--alert-#{$type}-background); + + .alert-title { + color: var(--alert-#{$type}-color); + } + } + } + } + + hr { + width: 100px; + margin: 40px auto; + background: var(--card-text-color-tertiary); + height: 2px; + border: 0; + opacity: 0.55; + } + + code { + color: var(--code-text-color); + background-color: var(--code-background-color); + padding: 2px 4px; + border-radius: var(--tag-border-radius); + font-family: var(--code-font-family); + } + + a, + code { + word-break: break-word; + } + + .gallery { + position: relative; + display: flex; + flex-direction: row; + justify-content: center; + margin: 1.5em 0; + gap: var(--spacing-sm); + + figure { + margin: 0; + } + } + + pre { + overflow-x: auto; + display: block; + background-color: var(--pre-background-color); + color: var(--pre-text-color); + font-family: var(--code-font-family); + line-height: 1.428571429; + word-break: break-all; + padding: var(--card-padding); + + // keep Codeblocks LTR + [dir="rtl"] & { + direction: ltr; + } + + code { + color: unset; + border: none; + background: none; + padding: 0; + } + } + + .highlight { + background-color: var(--pre-background-color); + padding: var(--card-padding); + position: relative; + + pre { + background-color: transparent; + } + + &:hover { + .copyCodeButton { + opacity: 1; + } + } + + // keep Codeblocks LTR + [dir="rtl"] & { + direction: ltr; + } + + pre { + padding: 0; + margin: 0; + width: auto; + } + } + + .copyCodeButton { + position: absolute; + top: calc(var(--card-padding)); + right: calc(var(--card-padding)); + background: var(--card-background); + border: none; + box-shadow: var(--shadow-l2); + border-radius: var(--tag-border-radius); + padding: 8px 16px; + color: var(--card-text-color-main); + cursor: pointer; + font-size: 14px; + opacity: 0; + transition: opacity 0.3s ease; + } + + .table-wrapper { + padding: 0 var(--card-padding); + overflow-x: auto; + display: block; + } + + table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + margin-bottom: 1.5em; + font-size: 0.96em; + } + + th, + td { + text-align: left; + padding: 4px 8px 4px 10px; + border: 1px solid var(--table-border-color); + } + + td { + vertical-align: top; + } + + tr:nth-child(even) { + background-color: var(--tr-even-background-color); + } + + .twitter-tweet { + color: var(--card-text-color-main); + } + + .video-wrapper { + position: relative; + width: 100%; + height: 0; + padding-bottom: 56.25%; + overflow: hidden; + + &>iframe, + &>video { + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + border: 0; + } + } + + .gitlab-embed-snippets { + margin: 0 !important; + + .file-holder.snippet-file-content { + margin-block-end: 0 !important; + margin-block-start: 0 !important; + margin-left: calc((var(--card-padding)) * -1) !important; + margin-right: calc((var(--card-padding)) * -1) !important; + padding: 0 var(--card-padding) !important; + } + } + + /// Negative margins + blockquote, + figure, + .highlight, + pre, + .gallery, + .video-wrapper, + .table-wrapper, + .s_video_simple { + margin-left: calc((var(--card-padding)) * -1); + margin-right: calc((var(--card-padding)) * -1); + width: calc(100% + var(--card-padding) * 2); + } + + /// Make long KaTeX equations scrollable in the x-axis + .katex-display>.katex { + overflow-x: auto; + overflow-y: hidden; + } + + kbd { + border: 1px solid var(--kbd-border-color); + font-weight: bold; + font-size: 0.9em; + line-height: 1; + padding: 2px 4px; + border-radius: 4px; + display: inline-block; + } +} \ No newline at end of file diff --git a/themes/stack/assets/scss/partials/layout/list.scss b/themes/stack/assets/scss/partials/layout/list.scss new file mode 100644 index 0000000..711bec0 --- /dev/null +++ b/themes/stack/assets/scss/partials/layout/list.scss @@ -0,0 +1,157 @@ +.section-card { + @include card-style; + padding: var(--small-card-padding); + display: flex; + align-items: center; + gap: var(--spacing-lg); + + --separation: 15px; + + .section-term { + font-size: 2.2rem; + margin: 0; + color: var(--card-text-color-main); + } + + .section-description { + font-weight: normal; + color: var(--card-text-color-secondary); + font-size: 1.6rem; + margin: 0; + } + + .section-details { + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 8px; + } + + .section-image { + img { + width: 60px; + height: 60px; + object-fit: cover; + } + } + + .section-count { + color: var(--card-text-color-tertiary); + font-size: 1.4rem; + margin: 0; + font-weight: bold; + text-transform: uppercase; + } +} + +@property --left-edge-fade-size { + syntax: ""; + inherits: false; + initial-value: 0px; +} + +@property --right-edge-fade-size { + syntax: ""; + inherits: false; + initial-value: 64px; +} + +.subsection-list { + --shadow-overlap-x: calc(var(--shadow-l2-blur) + 4px); + --shadow-overlap-y: calc(var(--shadow-l2-blur) + var(--shadow-l2-y) + 4px); + --edge-fade-size: 64px; + --left-edge-fade-size: 0px; + --right-edge-fade-size: var(--edge-fade-size); + + overflow-x: auto; + + /// Limit the horizontal overlap to the container padding to prevent page overflow on mobile. + /// On desktop, it still allows the full shadow to be visible. + --overlap-x: min(var(--shadow-overlap-x), var(--container-padding)); + + /// These values are computed from the shadow of the article-list--tile (which uses shadow-l1 and shadow-l2) + /// so that the shadow is not cut off, while staying within the viewport on small screens. + /// Clamp both sides to prevent page-level horizontal overflow. + margin-left: calc(var(--overlap-x) * -1); + margin-right: calc(var(--overlap-x) * -1); + margin-bottom: calc(var(--shadow-overlap-y) * -1); + + /// Smooth fade on both horizontal edges to avoid hard cuts where shadows are clamped. + --subsection-mask-image: linear-gradient( + to right, + transparent 0%, + rgb(0 0 0 / 0.06) calc(var(--left-edge-fade-size) * 0.12), + rgb(0 0 0 / 0.2) calc(var(--left-edge-fade-size) * 0.3), + rgb(0 0 0 / 0.45) calc(var(--left-edge-fade-size) * 0.52), + rgb(0 0 0 / 0.72) calc(var(--left-edge-fade-size) * 0.76), + black var(--left-edge-fade-size), + black calc(100% - var(--right-edge-fade-size)), + rgb(0 0 0 / 0.72) calc(100% - (var(--right-edge-fade-size) * 0.76)), + rgb(0 0 0 / 0.45) calc(100% - (var(--right-edge-fade-size) * 0.52)), + rgb(0 0 0 / 0.2) calc(100% - (var(--right-edge-fade-size) * 0.3)), + rgb(0 0 0 / 0.06) calc(100% - (var(--right-edge-fade-size) * 0.12)), + transparent 100% + ); + -webkit-mask-image: var(--subsection-mask-image); + mask-image: var(--subsection-mask-image); + + [dir="rtl"] & { + margin-left: calc(var(--overlap-x) * -1); + margin-right: calc(var(--overlap-x) * -1); + } + + .article-list--tile { + display: flex; + padding: var(--spacing-xs) var(--overlap-x) var(--shadow-overlap-y) var(--overlap-x); + width: max-content; + gap: var(--spacing-lg); + + [dir="rtl"] & { + padding: var(--spacing-xs) var(--overlap-x) var(--shadow-overlap-y) var(--overlap-x); + } + + article { + flex-shrink: 0; + + .article-title { + margin: 0; + font-size: 1.8rem; + } + + .article-details { + padding: var(--spacing-lg); + } + } + } +} + +// Only apply scroll-driven edge fades in browsers that support scroll timelines. +// Browsers without support (e.g. Firefox) fall back to the static right-edge fade +// defined by the explicit CSS variable declarations above. +@supports (animation-timeline: scroll(self inline)) { + .subsection-list { + animation: subsection-left-fade linear both, subsection-right-fade linear both; + animation-timeline: scroll(self inline); + animation-range: 1px 24px, 0% 100%; + animation-timing-function: ease-out, linear; + } +} + +@keyframes subsection-left-fade { + from { + --left-edge-fade-size: 0px; + } + to { + --left-edge-fade-size: var(--edge-fade-size); + } +} + +@keyframes subsection-right-fade { + 0%, + 92% { + --right-edge-fade-size: var(--edge-fade-size); + } + 100% { + --right-edge-fade-size: 0px; + } +} diff --git a/themes/stack/assets/scss/partials/layout/search.scss b/themes/stack/assets/scss/partials/layout/search.scss new file mode 100644 index 0000000..3f74da2 --- /dev/null +++ b/themes/stack/assets/scss/partials/layout/search.scss @@ -0,0 +1,87 @@ +.search-form { + position: relative; + --button-size: 80px; + + &.widget { + --button-size: 60px; + + label { + font-size: 1.3rem; + top: var(--spacing-sm); + } + + input { + font-size: 1.5rem; + padding: var(--spacing-2xl) var(--spacing-lg) var(--spacing-md) var(--spacing-lg); + } + } + + p { + position: relative; + margin: 0; + } + + label { + position: absolute; + top: var(--spacing-md); + inset-inline-start: var(--spacing-lg); + font-size: 1.4rem; + color: var(--card-text-color-tertiary); + } + + input { + padding: 40px 20px 20px; + @include card-style; + color: var(--card-text-color-main); + width: 100%; + border: 0; + -webkit-appearance: none; + appearance: none; + + transition: box-shadow 0.3s ease; + + font-size: 1.8rem; + + &:focus { + outline: 0; + box-shadow: var(--shadow-l2); + } + } + + button { + position: absolute; + inset-inline-end: 0; + top: 0; + height: 100%; + width: var(--button-size); + cursor: pointer; + background-color: transparent; + border: 0; + + padding: 0 10px; + + &:focus { + outline: 0; + + svg { + stroke-width: 2; + color: var(--accent-color); + } + } + + svg { + color: var(--card-text-color-secondary); + stroke-width: 1.33; + transition: all 0.3s ease; + width: 20px; + height: 20px; + } + } + +} + +.search-result { + &.hidden { + display: none; + } +} \ No newline at end of file diff --git a/themes/stack/assets/scss/partials/menu.scss b/themes/stack/assets/scss/partials/menu.scss new file mode 100644 index 0000000..6ac9752 --- /dev/null +++ b/themes/stack/assets/scss/partials/menu.scss @@ -0,0 +1,231 @@ +/*! + * Hamburgers + * @description Tasty CSS-animated hamburgers + * @author Jonathan Suh @jonsuh + * @site https://jonsuh.com/hamburgers + * @link https://github.com/jonsuh/hamburgers + */ + +.hamburger { + padding-top: var(--spacing-sm); + display: inline-block; + cursor: pointer; + transition-property: opacity, filter; + transition-duration: 0.15s; + transition-timing-function: linear; + font: inherit; + color: inherit; + text-transform: none; + background-color: transparent; + border: 0; + margin: 0; + overflow: visible; +} + +.hamburger:hover { + opacity: 0.7; +} + +.hamburger-box { + width: 30px; + height: 24px; + display: inline-block; + position: relative; +} + +.hamburger-inner { + display: block; + top: 50%; + margin-top: -2px; +} + +.hamburger-inner, +.hamburger-inner::before, +.hamburger-inner::after { + width: 30px; + height: 2px; + background-color: var(--card-text-color-main); + border-radius: 4px; + position: absolute; + transition-property: transform; + transition-duration: 0.15s; + transition-timing-function: ease; +} + +.hamburger-inner::before, +.hamburger-inner::after { + content: ""; + display: block; +} + +.hamburger-inner::before { + top: -10px; +} + +.hamburger-inner::after { + bottom: -10px; +} + +.hamburger--spin .hamburger-inner { + transition-duration: 0.22s; + transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); +} + +.hamburger--spin .hamburger-inner::before { + transition: top 0.1s 0.25s ease-in, opacity 0.1s ease-in; +} + +.hamburger--spin .hamburger-inner::after { + transition: bottom 0.1s 0.25s ease-in, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19); +} + +.hamburger--spin.is-active .hamburger-inner { + transform: rotate(225deg); + transition-delay: 0.12s; + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); +} + +.hamburger--spin.is-active .hamburger-inner::before { + top: 0; + opacity: 0; + transition: top 0.1s ease-out, opacity 0.1s 0.12s ease-out; +} + +.hamburger--spin.is-active .hamburger-inner::after { + bottom: 0; + transform: rotate(-90deg); + transition: bottom 0.1s ease-out, transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1); +} + +#toggle-menu { + background: none; + border: none; + position: absolute; + right: 0; + top: 0; + z-index: 2; + cursor: pointer; + + [dir="rtl"] & { + left: 0; + right: auto; + } + + @include respond(md) { + display: none; + } + + outline: none; + + &.is-active { + + .hamburger-inner, + .hamburger-inner::before, + .hamburger-inner::after { + background-color: var(--accent-color); + } + } +} + +/* Menu style */ +#main-menu { + list-style: none; + overflow-y: auto; + flex-grow: 1; + font-size: 1.4rem; + background-color: var(--card-background); + + box-shadow: var(--shadow-l1); + display: none; + margin: 0 calc(var(--container-padding) * -1); + + padding: var(--spacing-2xl); + + @include respond(xl) { + padding: 15px 0; + } + + &, + .menu-bottom-section ol { + flex-direction: column; + gap: var(--spacing-2xl); + + @include respond(xl) { + gap: var(--spacing-xl); + } + } + + &.show { + display: flex; + } + + @include respond(md) { + align-items: flex-end; + display: flex; + background-color: transparent; + padding: 0; + box-shadow: none; + margin: 0; + } + + li { + position: relative; + vertical-align: middle; + padding: 0; + + @include respond(md) { + width: 100%; + } + + svg { + stroke: currentColor; + stroke-width: 1.33; + width: 20px; + height: 20px; + } + + a { + height: 100%; + display: inline-flex; + align-items: center; + color: var(--body-text-color); + gap: var(--menu-icon-separation); + } + + span { + flex: 1; + } + + &.current { + a { + color: var(--accent-color); + font-weight: bold; + } + } + + &.menu-bottom-section { + margin-top: auto; + + ol { + display: flex; + padding-left: 0; + } + } + } +} + +.menu-social { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: row; + gap: var(--spacing-sm); + + svg { + width: 24px; + height: 24px; + stroke: var(--body-text-color); + stroke-width: 1.33; + } +} \ No newline at end of file diff --git a/themes/stack/assets/scss/partials/pagination.scss b/themes/stack/assets/scss/partials/pagination.scss new file mode 100644 index 0000000..108c34f --- /dev/null +++ b/themes/stack/assets/scss/partials/pagination.scss @@ -0,0 +1,259 @@ +.pagination { + display: flex; + @include card-style; + overflow: hidden; + + .hide-on-mobile, + .pagination-link { + display: none; + + @include respond(md) { + display: flex; + } + } + + .page-link { + padding: var(--spacing-sm) 0; + margin: 0; + border: 0; + background: none; + font: inherit; + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + flex: 1 1 auto; + min-width: 36px; + color: var(--card-text-color-secondary); + text-decoration: none; + + @include respond(md) { + min-width: 48px; + padding: var(--spacing-md) 0; + } + + &:first-child, + &:last-child { + flex: 0 0 var(--spacing-3xl); + padding: var(--spacing-sm); + + @include respond(md) { + flex: 0 0 48px; + padding: var(--spacing-md); + } + } + + &:hover:not(.current):not(.disabled) { + background-color: var(--card-background-selected); + opacity: 0.8; + } + + &.current { + background-color: var(--card-background-selected); + color: var(--card-text-color-main); + font-weight: bold; + cursor: default; + } + + &.disabled { + opacity: 0.3; + cursor: not-allowed; + pointer-events: none; + color: var(--card-text-color-secondary); + } + + svg { + width: 20px; + height: 20px; + } + + &.pagination-jump-trigger { + cursor: pointer; + } + } +} + +#pagination-jump-dialog { + border: none; + @include card-style; + box-shadow: var(--shadow-l2); + color: var(--body-text-color); + margin: auto; // Center dialog + width: calc(100% - (var(--container-padding) * 2)); + max-width: 320px; + padding: var(--card-padding); + + &::backdrop { + background-color: rgba(0, 0, 0, 0.4); + opacity: 0; + animation: backdrop-fade-in 0.3s ease forwards; + } + + &[open] { + animation: scale-up 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; + } + + &.closing { + animation: scale-down 0.2s ease forwards; + + &::backdrop { + animation: backdrop-fade-out 0.2s ease forwards; + } + } + + .pagination-jump-form { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + + header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--spacing-xs); + + label { + cursor: pointer; + } + + .title { + font-size: 1.6rem; + font-weight: 600; + color: var(--card-text-color-main); + margin: 0; + } + + .subtitle { + font-size: 1.3rem; + color: var(--card-text-color-tertiary); + } + } + + .pagination-jump-input-group { + display: flex; + gap: var(--spacing-sm); + align-items: stretch; + + input[type="number"] { + flex: 1; + width: 100%; + min-width: 0; + padding: 0 var(--spacing-md); + height: 44px; // Explicit height for perfect alignment + border: 1.5px solid var(--card-separator-color); + border-radius: 6px; + background-color: var(--body-background); + color: var(--card-text-color-main); + font-size: 1.6rem; + outline: none; + -webkit-appearance: none; + appearance: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + + /* Hide spinner */ + -moz-appearance: textfield; + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + &:focus, + &:focus-visible { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color) 20%, transparent); + } + } + + .btn-primary { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 var(--spacing-xl); + height: 44px; // Match input height + background-color: var(--accent-color); + color: var(--accent-color-text); + border: none; + border-radius: 6px; + font-size: 1.5rem; + font-weight: bold; + cursor: pointer; + transition: background-color 0.2s ease, box-shadow 0.2s ease; + + &:hover { + background-color: var(--accent-color-darker); + box-shadow: var(--shadow-l1); + } + + &:active { + box-shadow: none; + } + } + } + + .pagination-jump-hint { + display: flex; + align-items: center; + justify-content: flex-start; + gap: var(--spacing-sm); + font-size: 1.3rem; + color: var(--card-text-color-tertiary); + margin-top: calc(var(--spacing-xs) * -1); + + kbd { + background-color: var(--body-background); + border: 1px solid var(--card-separator-color); + border-radius: 4px; + padding: 3px 6px; + font-family: var(--base-font-family); + font-size: 1.1rem; + box-shadow: 0 2px 0 var(--card-separator-color); + color: var(--card-text-color-main); + font-weight: 600; + line-height: 1; + } + } + } + + @keyframes scale-up { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } + } + + @keyframes scale-down { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } + } + + @keyframes backdrop-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes backdrop-fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } + } +} diff --git a/themes/stack/assets/scss/partials/sidebar.scss b/themes/stack/assets/scss/partials/sidebar.scss new file mode 100644 index 0000000..58794b7 --- /dev/null +++ b/themes/stack/assets/scss/partials/sidebar.scss @@ -0,0 +1,228 @@ +.sidebar { + &.sticky { + @include respond(md) { + position: sticky; + } + } +} + +.left-sidebar { + display: flex; + flex-direction: column; + flex-shrink: 0; + align-self: stretch; + + @include respond(md) { + gap: var(--sidebar-element-separation); + } + + margin-bottom: calc(-1 * var(--sidebar-element-separation)); + + @include respond(md) { + margin-bottom: 0; + } + + &> :not(:last-child) { + margin-bottom: var(--sidebar-element-separation); + + @include respond(md) { + margin-bottom: 0; + } + } + + max-width: none; + width: 100%; + position: relative; + + --sidebar-avatar-size: 100px; + --sidebar-element-separation: var(--spacing-lg); + --emoji-size: 40px; + --emoji-font-size: 20px; + + @include respond(md) { + width: auto; + padding-top: var(--main-top-padding); + padding-bottom: var(--main-top-padding); + max-height: 100vh; + } + + @include respond(2xl) { + --sidebar-avatar-size: 120px; + --sidebar-element-separation: var(--spacing-xl); + --emoji-size: 40px; + } + + &.sticky { + top: 0; + } + + &.compact { + --sidebar-avatar-size: 80px; + --emoji-size: 30px; + --emoji-font-size: 15px; + + header { + @include respond(lg) { + flex-direction: row; + } + + .site-meta { + gap: var(--spacing-xs); + } + + .site-name { + font-size: 1.4rem; + + @include respond(2xl) { + font-size: 1.75rem; + } + } + + .site-description { + font-size: 1.4rem; + } + } + } +} + +.right-sidebar { + width: 100%; + display: none; + flex-direction: column; + gap: var(--widget-separation); + + &.sticky { + top: 0; + } + + @include respond(lg) { + padding-top: var(--main-top-padding); + padding-bottom: var(--main-top-padding); + } +} + +.sidebar header { + z-index: 1; + transition: box-shadow 0.5s ease; + display: flex; + flex-direction: column; + gap: var(--sidebar-element-separation); + + @include respond(md) { + padding: 0; + } + + .site-avatar { + position: relative; + margin: 0; + width: var(--sidebar-avatar-size); + height: var(--sidebar-avatar-size); + flex-shrink: 0; + + .site-logo { + width: 100%; + height: 100%; + border-radius: 100%; + box-shadow: var(--shadow-l1); + } + + .emoji { + position: absolute; + width: var(--emoji-size); + height: var(--emoji-size); + line-height: var(--emoji-size); + border-radius: 100%; + bottom: 0; + right: 0; + text-align: center; + font-size: var(--emoji-font-size); + background-color: var(--card-background); + box-shadow: var(--shadow-l2); + } + } + + .site-meta { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + justify-content: center; + } + + .site-name { + color: var(--accent-color); + margin: 0; + font-size: 1.6rem; + + @include respond(2xl) { + font-size: 1.8rem; + } + } + + .site-description { + color: var(--body-text-color); + font-weight: normal; + margin: 0; + font-size: 1.4rem; + + @include respond(2xl) { + font-size: 1.6rem; + } + } +} + +[data-scheme="dark"] { + #dark-mode-toggle { + color: var(--accent-color); + font-weight: 700; + + .icon-tabler-toggle-left { + display: none; + } + + .icon-tabler-toggle-right { + display: unset; + } + } +} + +#dark-mode-toggle { + margin-top: auto; + color: var(--body-text-color); + display: flex; + align-items: center; + cursor: pointer; + gap: var(--menu-icon-separation); + + .icon-tabler-toggle-right { + display: none; + } +} + +#i18n-switch { + color: var(--body-text-color); + display: inline-flex; + align-content: center; + gap: var(--menu-icon-separation); + + select { + border: 0; + background: none !important; + color: var(--body-text-color); + padding: 0 !important; + cursor: pointer; + font-family: inherit; + font-size: inherit; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + + &::-ms-expand { + display: none; + } + + option { + color: var(--card-text-color-main); + background-color: var(--card-background); + } + } +} \ No newline at end of file diff --git a/themes/stack/assets/scss/partials/widgets.scss b/themes/stack/assets/scss/partials/widgets.scss new file mode 100644 index 0000000..c29fbbb --- /dev/null +++ b/themes/stack/assets/scss/partials/widgets.scss @@ -0,0 +1,64 @@ +.widget { + display: flex; + flex-direction: column; + + .widget-icon { + svg { + width: var(--widget-icon-size); + height: var(--widget-icon-size); + stroke-width: 1.6; + color: var(--body-text-color); + } + } +} + +/* Tag cloud widget */ +.tagCloud { + .tagCloud-tags { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-sm); + + a { + @include card-style; + border-radius: var(--tag-border-radius); + padding: 8px 20px; + color: var(--card-text-color-main); + font-size: 1.4rem; + transition: box-shadow 0.3s ease; + + &:hover { + box-shadow: var(--shadow-l2); + } + } + } +} + +/* Archives widget */ +.widget.archives { + .widget-archive--list { + @include card-style; + } + + .archives-year { + &:not(:last-of-type) { + border-bottom: 1.5px solid var(--card-separator-color); + } + + a { + font-size: 1.4rem; + padding: 18px 25px; + display: flex; + + span.year { + flex: 1; + color: var(--card-text-color-main); + font-weight: bold; + } + + span.count { + color: var(--card-text-color-tertiary); + } + } + } +} diff --git a/themes/stack/assets/scss/style.scss b/themes/stack/assets/scss/style.scss new file mode 100644 index 0000000..a7a6c7a --- /dev/null +++ b/themes/stack/assets/scss/style.scss @@ -0,0 +1,30 @@ +/*! +* Hugo Theme Stack +* +* @author: Jimmy Cai +* @website: https://jimmycai.com +* @link: https://github.com/CaiJimmy/hugo-theme-stack +*/ + +@import "breakpoints.scss"; +@import "variables.scss"; +@import "mixins.scss"; +@import "grid.scss"; + +@import "external/normalize.scss"; + +@import "partials/menu.scss"; +@import "partials/article.scss"; +@import "partials/widgets.scss"; +@import "partials/footer.scss"; +@import "partials/pagination.scss"; +@import "partials/sidebar.scss"; +@import "partials/base.scss"; +@import "partials/layout/article.scss"; +@import "partials/layout/list.scss"; +@import "partials/layout/404.scss"; +@import "partials/layout/search.scss"; +@import "partials/cookies.scss"; + +@import "general.scss"; +@import "custom.scss"; diff --git a/themes/stack/assets/scss/variables.scss b/themes/stack/assets/scss/variables.scss new file mode 100644 index 0000000..766dffc --- /dev/null +++ b/themes/stack/assets/scss/variables.scss @@ -0,0 +1,228 @@ +/* +* Global style +*/ +:root { + --main-top-padding: 35px; + + @include respond(xl) { + --main-top-padding: 50px; + } + + --body-background: #f5f5fa; + + --accent-color: #34495e; + --accent-color-darker: #2c3e50; + --accent-color-text: #fff; + --body-text-color: #707070; + + --tag-border-radius: 4px; + + --section-separation: 40px; + + &[data-scheme="dark"] { + --body-background: #303030; + --accent-color: #ecf0f1; + --accent-color-darker: #bdc3c7; + --accent-color-text: #000; + --body-text-color: rgba(255, 255, 255, 0.7); + } +} + +/** +* Global font family +*/ +:root { + --sys-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Droid Sans", "Helvetica Neue"; + --base-font-family: "Lato", var(--sys-font-family), sans-serif; + --code-font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} + +/* +* Card style +*/ +:root { + --card-background: #fff; + --card-background-selected: #eaeaea; + + --card-text-color-main: #000; + --card-text-color-secondary: #747474; + --card-text-color-tertiary: #767676; + --card-separator-color: rgba(218, 218, 218, 0.5); + + --card-border-radius: 10px; + + --card-padding: 20px; + + @include respond(md) { + --card-padding: 25px; + } + + @include respond(xl) { + --card-padding: 30px; + } + + --small-card-padding: 25px 20px; + + @include respond(md) { + --small-card-padding: 25px; + } + + &[data-scheme="dark"] { + --card-background: #424242; + --card-background-selected: rgba(255, 255, 255, 0.16); + --card-text-color-main: rgba(255, 255, 255, 0.9); + --card-text-color-secondary: rgba(255, 255, 255, 0.7); + --card-text-color-tertiary: rgba(255, 255, 255, 0.5); + --card-separator-color: rgba(255, 255, 255, 0.12); + } +} + +/** +* Article content font settings +*/ +:root { + --article-font-family: var(--base-font-family); + --article-font-size: 1.6rem; + + @include respond(md) { + --article-font-size: 1.7rem; + } + + --article-line-height: 1.85; +} + +/* +* Article content style +*/ +:root { + --blockquote-border-size: 4px; + --blockquote-background-color: rgb(248 248 248); + + --heading-border-size: 4px; + + --alert-note-color: #4da3ff; + --alert-note-background: rgba(77, 163, 255, 0.08); + --alert-tip-color: #3fb950; + --alert-tip-background: rgba(63, 185, 80, 0.08); + --alert-important-color: #8457d9; + --alert-important-background: rgba(130, 80, 223, 0.08); + --alert-warning-color: #d29922; + --alert-warning-background: rgba(210, 153, 34, 0.1); + --alert-caution-color: #f85149; + --alert-caution-background: rgba(248, 81, 73, 0.1); + + --link-background-color: 189, 195, 199; + --link-background-opacity: 0.5; + --link-background-opacity-hover: 0.7; + + --code-background-color: rgba(0, 0, 0, 0.12); + --code-text-color: #808080; + + --table-border-color: #dadada; + --tr-even-background-color: #efefee; + + --kbd-border-color: #dadada; + + &[data-scheme="dark"] { + --code-background-color: #272822; + --code-text-color: rgba(255, 255, 255, 0.9); + + --table-border-color: #717171; + --tr-even-background-color: #545454; + + --blockquote-background-color: rgb(75 75 75); + } +} + +/* +* Shadow style +* Thanks to https://www.figma.com/community/plugin/744987207861965946/Shadow-picker +*/ +:root { + --shadow-l1-y: 4px; + --shadow-l1-blur: 8px; + --shadow-l2-y: 10px; + --shadow-l2-blur: 20px; + + --shadow-l1: 0px var(--shadow-l1-y) var(--shadow-l1-blur) rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.06), 0px 0px 1px rgba(0, 0, 0, 0.04); + --shadow-l2: 0px var(--shadow-l2-y) var(--shadow-l2-blur) rgba(0, 0, 0, 0.04), 0px 2px 6px rgba(0, 0, 0, 0.04), 0px 0px 1px rgba(0, 0, 0, 0.04); +} + +[data-scheme="light"] { + --pre-text-color: #272822; + --pre-background-color: #fafafa; + @import "partials/highlight/light.scss"; +} + +[data-scheme="dark"] { + --pre-text-color: #f8f8f2; + --pre-background-color: #272822; + @import "partials/highlight/dark.scss"; +} + +:root { + --menu-icon-separation: 40px; + --container-padding: 15px; + + @include respond(md) { + --container-padding: 20px; + } + + --widget-separation: var(--section-separation); + + /* Spacing tokens */ + --spacing-xs: 5px; + --spacing-sm: 10px; + --spacing-md: 15px; + --spacing-lg: 20px; + --spacing-xl: 25px; + --spacing-2xl: 30px; + --spacing-3xl: 40px; + + /* Article image height variables */ + --article-image-height: 150px; + + @include respond(md) { + --article-image-height: 200px; + } + + @include respond(xl) { + --article-image-height: 250px; + } + + /* Mermaid component variables */ + --mermaid-toolbar-btn-bg: rgba(255, 255, 255, 0.95); + --mermaid-toolbar-btn-text: #333; + --mermaid-modal-bg: rgba(0, 0, 0, 0.95); + --mermaid-modal-content-bg: #fff; + --mermaid-modal-btn-bg: #fff; + --mermaid-modal-btn-hover: #ddd; + + &[data-scheme="dark"] { + --mermaid-toolbar-btn-bg: rgba(0, 0, 0, 0.85); + --mermaid-toolbar-btn-text: #eee; + --mermaid-modal-content-bg: #1e1e1e; + --mermaid-modal-btn-bg: #333; + --mermaid-modal-btn-hover: #444; + } + + /* Article compact image size */ + --article-compact-image-size: 50px; + + @include respond(md) { + --article-compact-image-size: 60px; + } + + /* Widget icon size */ + --widget-icon-size: 32px; + + /* Article list tile variables */ + --article-tile-width: 250px; + --article-tile-height: 150px; + --article-tile-overlay-bg: rgba(0, 0, 0, 0.25); + --article-tile-text-color: #fff; + + &[data-scheme="dark"] { + --article-tile-overlay-bg: rgba(0, 0, 0, 0.45); + } +} \ No newline at end of file diff --git a/themes/stack/assets/ts/code-copy.ts b/themes/stack/assets/ts/code-copy.ts new file mode 100644 index 0000000..995223e --- /dev/null +++ b/themes/stack/assets/ts/code-copy.ts @@ -0,0 +1,41 @@ +import * as params from '@params'; + +export function setupCodeCopy() { + /** + * Add copy button to code block + */ + const highlights = document.querySelectorAll('.article-content div.highlight'); + const copyText = params.codeblock.copy, + copiedText = params.codeblock.copied; + + if (!navigator.clipboard) { + /// Clipboard API is only supported in secure contexts (HTTPS) + console.warn('Clipboard API not supported, copy button will not work.'); + return; + } + + highlights.forEach(highlight => { + const copyButton = document.createElement('button'); + copyButton.innerHTML = copyText; + copyButton.classList.add('copyCodeButton'); + highlight.appendChild(copyButton); + + const codeBlock = highlight.querySelector('code[data-lang]'); + if (!codeBlock) return; + + copyButton.addEventListener('click', () => { + navigator.clipboard.writeText(codeBlock.textContent) + .then(() => { + copyButton.textContent = copiedText; + + setTimeout(() => { + copyButton.textContent = copyText; + }, 1000); + }) + .catch(err => { + alert(err) + console.log('Something went wrong', err); + }); + }); + }); +}; \ No newline at end of file diff --git a/themes/stack/assets/ts/colorScheme.ts b/themes/stack/assets/ts/colorScheme.ts new file mode 100644 index 0000000..978e98e --- /dev/null +++ b/themes/stack/assets/ts/colorScheme.ts @@ -0,0 +1,92 @@ +type colorScheme = 'light' | 'dark' | 'auto'; + +class StackColorScheme { + private localStorageKey = 'StackColorScheme'; + private currentScheme: colorScheme; + private systemPreferScheme: colorScheme; + + constructor(toggleEl: HTMLElement) { + this.bindMatchMedia(); + this.currentScheme = this.getSavedScheme(); + if (window.matchMedia('(prefers-color-scheme: dark)').matches === true) + this.systemPreferScheme = 'dark' + else + this.systemPreferScheme = 'light'; + + this.dispatchEvent(document.documentElement.dataset.scheme as colorScheme); + + if (toggleEl) + this.bindClick(toggleEl); + + if (document.body.style.transition == '') + document.body.style.setProperty('transition', 'background-color .3s ease'); + } + + private saveScheme() { + localStorage.setItem(this.localStorageKey, this.currentScheme); + } + + private bindClick(toggleEl: HTMLElement) { + toggleEl.addEventListener('click', (e) => { + if (this.isDark()) { + /// Disable dark mode + this.currentScheme = 'light'; + } + else { + this.currentScheme = 'dark'; + } + + this.setBodyClass(); + + if (this.currentScheme == this.systemPreferScheme) { + /// Set to auto + this.currentScheme = 'auto'; + } + + this.saveScheme(); + }) + } + + private isDark() { + return (this.currentScheme == 'dark' || this.currentScheme == 'auto' && this.systemPreferScheme == 'dark'); + } + + private dispatchEvent(colorScheme: colorScheme) { + const event = new CustomEvent('onColorSchemeChange', { + detail: colorScheme + }); + window.dispatchEvent(event); + } + + private setBodyClass() { + if (this.isDark()) { + document.documentElement.dataset.scheme = 'dark'; + } + else { + document.documentElement.dataset.scheme = 'light'; + } + + this.dispatchEvent(document.documentElement.dataset.scheme as colorScheme); + } + + private getSavedScheme(): colorScheme { + const savedScheme = localStorage.getItem(this.localStorageKey); + + if (savedScheme == 'light' || savedScheme == 'dark' || savedScheme == 'auto') return savedScheme; + else return 'auto'; + } + + private bindMatchMedia() { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + if (e.matches) { + this.systemPreferScheme = 'dark'; + } + else { + this.systemPreferScheme = 'light'; + } + this.setBodyClass(); + }); + } +} + +export default StackColorScheme; diff --git a/themes/stack/assets/ts/commentsConsent.ts b/themes/stack/assets/ts/commentsConsent.ts new file mode 100644 index 0000000..a38553f --- /dev/null +++ b/themes/stack/assets/ts/commentsConsent.ts @@ -0,0 +1,141 @@ +/* + * Consent-gated comments bootstrap. + * + * This module controls whether third-party comments are rendered based on + * functional cookie consent, and injects template scripts in sequence to + * avoid eager or duplicate script execution. + */ + +/** + * Consent event payload used by the comments gate. + * It mirrors cookie consent state shape but keeps optional fields for safety. + */ +interface CommentsConsentState { + necessary?: boolean; + analytics?: boolean; + functional?: boolean; + timestamp?: number; +} + +/** + * Resolve functional consent with layered fallback: + * 1) explicit event detail, 2) CookieConsent public API, 3) dataset mirror. + */ +const hasFunctionalConsent = (consentDetail?: CommentsConsentState | null): boolean => { + const cookieConsent = (window as Window & { + cookieConsent?: { + hasConsent?: (category: 'necessary' | 'analytics' | 'functional') => boolean; + }; + }).cookieConsent; + + return ( + consentDetail?.functional ?? + cookieConsent?.hasConsent?.('functional') ?? + document.documentElement.dataset.consentFunctional === 'true' + ); +}; + +interface DeferredScript { + placeholder: Comment; + script: HTMLScriptElement; +} + +const prepareDeferredScripts = (fragment: DocumentFragment): DeferredScript[] => { + return Array.from(fragment.querySelectorAll('script')).map(script => { + const placeholder = document.createComment('comments-script-placeholder'); + script.replaceWith(placeholder); + return { placeholder, script }; + }); +}; + +const activateDeferredScripts = async (scripts: DeferredScript[]): Promise => { + // Inject scripts sequentially so third-party embeds can rely on execution order. + for (const { placeholder, script } of scripts) { + if (!placeholder.parentNode) { + continue; + } + + const newScript = document.createElement('script'); + Array.from(script.attributes).forEach(attr => { + newScript.setAttribute(attr.name, attr.value); + }); + + if (script.textContent) { + newScript.text = script.textContent; + } + + let done: Promise = Promise.resolve(); + if (newScript.src) { + done = new Promise(resolve => { + newScript.onload = newScript.onerror = () => resolve(); + }); + } + + placeholder.replaceWith(newScript); + await done; + } +}; + +const initCommentsConsent = (): void => { + // Main entry: gate comments rendering by functional cookie consent. + const placeholder = document.getElementById('comments-consent-placeholder') as HTMLElement | null; + const container = document.getElementById('comments-container') as HTMLElement | null; + const template = document.getElementById('comments-template') as HTMLTemplateElement | null; + + if (!placeholder || !container || !template) { + return; + } + + let commentsLoaded = false; + let commentsLoading = false; + + const showComments = async (): Promise => { + placeholder.style.display = 'none'; + container.style.display = 'block'; + + if (commentsLoaded || commentsLoading) { + return; + } + + // Prevent duplicate template hydration while scripts are still loading. + commentsLoading = true; + + try { + const clone = template.content.cloneNode(true) as DocumentFragment; + const scripts = prepareDeferredScripts(clone); + + container.appendChild(clone); + await activateDeferredScripts(scripts); + commentsLoaded = true; + } finally { + commentsLoading = false; + } + }; + + const hideComments = (): void => { + placeholder.style.display = 'block'; + container.style.display = 'none'; + }; + + window.addEventListener('onCookieConsentChange', (event: Event) => { + // Cross-module contract: this event is dispatched by cookies.ts. + const customEvent = event as CustomEvent; + if (hasFunctionalConsent(customEvent.detail)) { + showComments(); + } else { + hideComments(); + } + }); + + if (hasFunctionalConsent()) { + showComments(); + } else { + hideComments(); + } +}; + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCommentsConsent, { once: true }); +} else { + initCommentsConsent(); +} diff --git a/themes/stack/assets/ts/cookies.ts b/themes/stack/assets/ts/cookies.ts new file mode 100644 index 0000000..830057b --- /dev/null +++ b/themes/stack/assets/ts/cookies.ts @@ -0,0 +1,222 @@ +interface ConsentState { + necessary: boolean; + analytics: boolean; + functional: boolean; + timestamp: number; +} + +class CookieConsent { + private static COOKIE_NAME = 'cookie_consent'; + private static COOKIE_DAYS = 365; + + private state: ConsentState | null = null; + private banner: HTMLElement | null = null; + private settingsPanel: HTMLElement | null = null; + + constructor() { + this.banner = document.getElementById('cookie-consent-banner'); + this.settingsPanel = document.getElementById('cookie-settings-panel'); + + this.state = this.loadState(); + + if (!this.state && this.banner) { + this.showBanner(); + } + + this.bindEvents(); + this.dispatchConsentEvent(); + } + + private loadState(): ConsentState | null { + const cookie = document.cookie + .split('; ') + .find(row => row.startsWith(CookieConsent.COOKIE_NAME + '=')); + + if (!cookie) return null; + + try { + return JSON.parse(decodeURIComponent(cookie.split('=')[1])); + } catch { + return null; + } + } + + private saveState(): void { + if (!this.state) return; + + const expires = new Date(); + expires.setDate(expires.getDate() + CookieConsent.COOKIE_DAYS); + + document.cookie = `${CookieConsent.COOKIE_NAME}=${encodeURIComponent(JSON.stringify(this.state))}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`; + } + + private showBanner(): void { + if (this.banner) { + this.banner.removeAttribute('aria-hidden'); + } + } + + private hideBanner(): void { + if (this.banner) { + // Blur any focused element inside the banner before hiding + const activeElement = document.activeElement as HTMLElement; + if (activeElement && this.banner.contains(activeElement)) { + activeElement.blur(); + } + this.banner.setAttribute('aria-hidden', 'true'); + } + this.hideSettings(); + } + + private showSettings(): void { + if (this.settingsPanel) { + this.settingsPanel.removeAttribute('aria-hidden'); + + // Restore checkbox states from current state or defaults + const checkboxes = this.settingsPanel.querySelectorAll('input[data-cookie-category]'); + checkboxes.forEach((cb) => { + const input = cb as HTMLInputElement; + const category = input.dataset.cookieCategory as keyof ConsentState; + if (category && this.state && typeof this.state[category] === 'boolean') { + input.checked = this.state[category] as boolean; + } else { + input.checked = false; + } + }); + } + } + + private hideSettings(): void { + if (this.settingsPanel) { + // Blur any focused element inside the settings panel before hiding + const activeElement = document.activeElement as HTMLElement; + if (activeElement && this.settingsPanel.contains(activeElement)) { + activeElement.blur(); + } + this.settingsPanel.setAttribute('aria-hidden', 'true'); + } + } + + private bindEvents(): void { + document.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + const action = target.dataset.cookieAction; + + if (!action) return; + + switch (action) { + case 'accept': + this.acceptAll(); + break; + case 'deny': + this.denyAll(); + break; + case 'settings': + this.showSettings(); + break; + case 'save': + this.saveSettings(); + break; + case 'cancel': + this.hideSettings(); + break; + case 'reopen': + this.showBanner(); + break; + } + }); + } + + private acceptAll(): void { + this.state = { + necessary: true, + analytics: true, + functional: true, + timestamp: Date.now() + }; + this.saveState(); + this.hideBanner(); + this.dispatchConsentEvent(); + } + + private denyAll(): void { + this.state = { + necessary: true, + analytics: false, + functional: false, + timestamp: Date.now() + }; + this.saveState(); + this.hideBanner(); + this.dispatchConsentEvent(); + } + + private saveSettings(): void { + const checkboxes = document.querySelectorAll('input[data-cookie-category]'); + + this.state = { + necessary: true, + analytics: false, + functional: false, + timestamp: Date.now() + }; + + checkboxes.forEach((cb) => { + const input = cb as HTMLInputElement; + const category = input.dataset.cookieCategory as keyof ConsentState; + if (category && category in this.state!) { + (this.state as any)[category] = input.checked; + } + }); + + this.saveState(); + this.hideBanner(); + this.dispatchConsentEvent(); + } + + private dispatchConsentEvent(): void { + // Cross-module event consumed by consent-gated features (for example commentsConsent.ts). + const event = new CustomEvent('onCookieConsentChange', { + detail: this.state + }); + window.dispatchEvent(event); + + // Set data attributes on document for CSS-based control + if (this.state) { + document.documentElement.dataset.consentAnalytics = String(this.state.analytics); + document.documentElement.dataset.consentFunctional = String(this.state.functional); + } + } + + // Public API + public hasConsent(category: keyof Omit): boolean { + if (!this.state) return false; + return this.state[category] ?? false; + } + + public getState(): ConsentState | null { + return this.state; + } + + public reopenBanner(): void { + this.showBanner(); + } +} + +// Export for module usage +export default CookieConsent; + +// Initialize when DOM is ready and expose globally +declare global { + interface Window { + cookieConsent: CookieConsent; + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + window.cookieConsent = new CookieConsent(); + }); +} else { + window.cookieConsent = new CookieConsent(); +} diff --git a/themes/stack/assets/ts/createElement.ts b/themes/stack/assets/ts/createElement.ts new file mode 100644 index 0000000..3a1e85e --- /dev/null +++ b/themes/stack/assets/ts/createElement.ts @@ -0,0 +1,34 @@ +/** + * createElement + * Edited from: + * @link https://stackoverflow.com/a/42405694 + */ +function createElement(tag, attrs, children) { + var element = document.createElement(tag); + + for (let name in attrs) { + if (name && attrs.hasOwnProperty(name)) { + let value = attrs[name]; + + if (name == "dangerouslySetInnerHTML") { + element.innerHTML = value.__html; + } + else if (value === true) { + element.setAttribute(name, name); + } else if (value !== false && value != null) { + element.setAttribute(name, value.toString()); + } + } + } + for (let i = 2; i < arguments.length; i++) { + let child = arguments[i]; + if (child) { + element.appendChild( + child.nodeType == null ? + document.createTextNode(child.toString()) : child); + } + } + return element; +} + +export default createElement; \ No newline at end of file diff --git a/themes/stack/assets/ts/gallery.ts b/themes/stack/assets/ts/gallery.ts new file mode 100644 index 0000000..9db9c3f --- /dev/null +++ b/themes/stack/assets/ts/gallery.ts @@ -0,0 +1,110 @@ +const wrap = (figures: HTMLElement[]) => { + const galleryContainer = document.createElement('div'); + galleryContainer.className = 'gallery'; + + const parentNode = figures[0].parentNode!, + first = figures[0]; + + parentNode.insertBefore(galleryContainer, first) + + for (const figure of figures) { + galleryContainer.appendChild(figure); + } +} + +const unescapeHtml = (html: string) => { + /// Replace special HTML entities with their corresponding characters + return html.replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'"); +} + +export default (container: HTMLElement) => { + /// The process of wrapping image with figure tag is done using JavaScript instead of only Hugo markdown render hook + /// because it can not detect whether image is being wrapped by a link or not + /// and it lead to a invalid HTML construction (
) + const images = container.querySelectorAll('img.gallery-image') as NodeListOf; + for (const img of Array.from(images)) { + /// Images are wrapped with figure tag if the paragraph has only images without texts + /// This is done to allow inline images within paragraphs + const paragraph = img.closest('p'); + + if (!paragraph || !container.contains(paragraph)) continue; + + if (paragraph.textContent.trim() == '') { + /// Once we insert figcaption, this check no longer works + /// So we add a class to paragraph to mark it + paragraph.classList.add('no-text'); + } + + let isNewLineImage = paragraph.classList.contains('no-text'); + if (!isNewLineImage) continue; + + const hasLink = img.parentElement!.tagName == 'A'; + + let el: HTMLElement = img; + /// Wrap image with figure tag, with flex-grow and flex-basis values extracted from img's data attributes + const figure = document.createElement('figure'); + figure.classList.add('gallery-image'); + figure.style.setProperty('flex-grow', img.getAttribute('data-flex-grow') || '1'); + figure.style.setProperty('flex-basis', img.getAttribute('data-flex-basis') || '0'); + + if (hasLink) { + /// Wrap if it exists + el = img.parentElement!; + el.classList.add('image-link'); + el.setAttribute('data-pswp-width', img.getAttribute('width')!); + el.setAttribute('data-pswp-height', img.getAttribute('height')!); + } else { + const a = document.createElement('a'); + a.href = img.src; + a.setAttribute('class', 'image-link'); + a.setAttribute('target', '_blank'); + a.setAttribute('data-pswp-width', img.getAttribute('width')!); + a.setAttribute('data-pswp-height', img.getAttribute('height')!); + img.parentNode!.insertBefore(a, img); + a.appendChild(img); + el = a; + } + + el.parentElement!.insertBefore(figure, el); + figure.appendChild(el); + + /// Add figcaption if it exists + let caption = img.getAttribute('alt'); + if (img.getAttribute('data-title-escaped')) { + caption = unescapeHtml(img.getAttribute('data-title-escaped')!); + } + if (caption) { + const figcaption = document.createElement('figcaption'); + figcaption.innerHTML = caption; + figure.appendChild(figcaption); + } + } + + const figuresEl = container.querySelectorAll('figure.gallery-image') as NodeListOf; + + let currentGallery: HTMLElement[] = []; + + for (const figure of Array.from(figuresEl)) { + if (!currentGallery.length) { + /// First iteration + currentGallery = [figure]; + } + else if (figure.previousElementSibling === currentGallery[currentGallery.length - 1]) { + /// Adjacent figures + currentGallery.push(figure); + } + else if (currentGallery.length) { + /// End gallery + wrap(currentGallery); + currentGallery = [figure]; + } + } + + if (currentGallery.length > 0) { + wrap(currentGallery); + } +}; \ No newline at end of file diff --git a/themes/stack/assets/ts/main.ts b/themes/stack/assets/ts/main.ts new file mode 100644 index 0000000..408fc87 --- /dev/null +++ b/themes/stack/assets/ts/main.ts @@ -0,0 +1,50 @@ +/*! +* Hugo Theme Stack +* +* @author: Jimmy Cai +* @website: https://jimmycai.com +* @link: https://github.com/CaiJimmy/hugo-theme-stack +*/ +import menu from './menu'; +import createElement from './createElement'; +import StackColorScheme from './colorScheme'; +import { setupScrollspy } from './scrollspy'; +import { setupSmoothAnchors } from './smoothAnchors'; +import { setupPaginationJump } from './pagination'; +import { setupCodeCopy } from './code-copy'; + +let Stack = { + init: () => { + /** + * Bind menu event + */ + menu(); + + const articleContent = document.querySelector('.article-content') as HTMLElement; + if (articleContent) { + setupSmoothAnchors(); + setupScrollspy(); + setupCodeCopy(); + } + + setupPaginationJump(); + + new StackColorScheme(document.getElementById('dark-mode-toggle')!); + } +} + +window.addEventListener('load', () => { + setTimeout(function () { + Stack.init(); + }, 0); +}) + +declare global { + interface Window { + createElement: any; + Stack: any + } +} + +window.Stack = Stack; +window.createElement = createElement; \ No newline at end of file diff --git a/themes/stack/assets/ts/menu.ts b/themes/stack/assets/ts/menu.ts new file mode 100644 index 0000000..34615ba --- /dev/null +++ b/themes/stack/assets/ts/menu.ts @@ -0,0 +1,83 @@ +/** + * Slide up/down + * Code from https://dev.to/bmsvieira/vanilla-js-slidedown-up-4dkn + * @param target + * @param duration + */ +let slideUp = (target: HTMLElement, duration = 500) => { + target.classList.add('transiting'); + target.style.transitionProperty = 'height, margin, padding'; + target.style.transitionDuration = duration + 'ms'; + ///target.style.boxSizing = 'border-box'; + target.style.height = target.offsetHeight + 'px'; + target.offsetHeight; + target.style.overflow = 'hidden'; + target.style.height = "0"; + target.style.paddingTop = "0"; + target.style.paddingBottom = "0"; + target.style.marginTop = "0"; + target.style.marginBottom = "0"; + window.setTimeout(() => { + target.classList.remove('show') + target.style.removeProperty('height'); + target.style.removeProperty('padding-top'); + target.style.removeProperty('padding-bottom'); + target.style.removeProperty('margin-top'); + target.style.removeProperty('margin-bottom'); + target.style.removeProperty('overflow'); + target.style.removeProperty('transition-duration'); + target.style.removeProperty('transition-property'); + target.classList.remove('transiting'); + }, duration); +} + +let slideDown = (target: HTMLElement, duration = 500) => { + target.classList.add('transiting'); + target.style.removeProperty('display'); + + target.classList.add('show'); + + let height = target.offsetHeight; + target.style.overflow = 'hidden'; + target.style.height = "0"; + target.style.paddingTop = "0"; + target.style.paddingBottom = "0"; + target.style.marginTop = "0"; + target.style.marginBottom = "0"; + target.offsetHeight; + ///target.style.boxSizing = 'border-box'; + target.style.transitionProperty = "height, margin, padding"; + target.style.transitionDuration = duration + 'ms'; + target.style.height = height + 'px'; + target.style.removeProperty('padding-top'); + target.style.removeProperty('padding-bottom'); + target.style.removeProperty('margin-top'); + target.style.removeProperty('margin-bottom'); + window.setTimeout(() => { + target.style.removeProperty('height'); + target.style.removeProperty('overflow'); + target.style.removeProperty('transition-duration'); + target.style.removeProperty('transition-property'); + target.classList.remove('transiting'); + }, duration); +} + +let slideToggle = (target, duration = 500) => { + if (window.getComputedStyle(target).display === 'none') { + return slideDown(target, duration); + } else { + return slideUp(target, duration); + } +} + +export default function () { + const toggleMenu = document.getElementById('toggle-menu'); + if (toggleMenu) { + toggleMenu.addEventListener('click', () => { + if (document.getElementById('main-menu').classList.contains('transiting')) return; + document.body.classList.toggle('show-menu'); + slideToggle(document.getElementById('main-menu'), 300); + toggleMenu.classList.toggle('is-active'); + }); + } +} \ No newline at end of file diff --git a/themes/stack/assets/ts/mermaid.ts b/themes/stack/assets/ts/mermaid.ts new file mode 100644 index 0000000..f05dd36 --- /dev/null +++ b/themes/stack/assets/ts/mermaid.ts @@ -0,0 +1,235 @@ +declare const mermaid: { + initialize(config: Record): void; + run(options: { nodes: HTMLElement[] }): Promise; +}; + +interface MermaidConfig { + transparentBackground?: boolean; + lightTheme?: string; + darkTheme?: string; + lightThemeVariables?: Record; + darkThemeVariables?: Record; + securityLevel?: string; + look?: string; + htmlLabels?: boolean; + maxTextSize?: number; + maxEdges?: number; + fontSize?: number; + fontFamily?: string; + curve?: string; + logLevel?: number; +} + +type Scheme = 'light' | 'dark'; + +const PANZOOM_CDN = 'https://cdn.jsdelivr.net/npm/panzoom@9.4.3/+esm'; + +function getScheme(): Scheme { + return document.documentElement.dataset.scheme === 'dark' ? 'dark' : 'light'; +} + +function buildThemeConfig(cfg: MermaidConfig, scheme: Scheme) { + const isLight = scheme === 'light'; + const theme = isLight ? (cfg.lightTheme ?? 'default') : (cfg.darkTheme ?? 'dark'); + const vars = isLight ? (cfg.lightThemeVariables ?? {}) : (cfg.darkThemeVariables ?? {}); + return { + theme, + themeVariables: { ...vars, ...(cfg.transparentBackground ? { background: 'transparent' } : {}) }, + }; +} + +function buildBaseConfig(cfg: MermaidConfig): Record { + const base: Record = { + startOnLoad: false, + securityLevel: cfg.securityLevel ?? 'strict', + look: cfg.look ?? 'classic', + flowchart: { htmlLabels: cfg.htmlLabels ?? true, useMaxWidth: true }, + gantt: { useWidth: 800 }, + }; + const optional: (keyof MermaidConfig)[] = ['maxTextSize', 'maxEdges', 'fontSize', 'fontFamily', 'curve', 'logLevel']; + for (const key of optional) { + if (cfg[key] != null) base[key] = cfg[key]; + } + return base; +} + +function initWithTheme( + scheme: Scheme, + themes: Record>, + baseConfig: Record, +) { + const { theme, themeVariables } = themes[scheme]; + mermaid.initialize({ + ...baseConfig, + theme, + ...(Object.keys(themeVariables).length && { themeVariables }), + }); +} + +async function renderOffscreen(sources: string[]): Promise { + const container = document.createElement('div'); + container.className = 'mermaid-offscreen'; + document.body.appendChild(container); + const nodes = sources.map(src => { + const n = document.createElement('pre'); + n.innerHTML = src; + container.appendChild(n); + return n; + }); + await mermaid.run({ nodes }); + const results = nodes.map(n => n.innerHTML); + container.remove(); + return results; +} + +function setupWrappers(elements: NodeListOf) { + elements.forEach((el, idx) => { + const wrapper = document.createElement('div'); + wrapper.className = 'mermaid-wrapper'; + el.parentNode!.insertBefore(wrapper, el); + wrapper.appendChild(el); + wrapper.insertAdjacentHTML( + 'beforeend', + `
`, + ); + }); +} + +function setupModal(elements: NodeListOf) { + const modal = document.getElementById('mermaid-modal')!; + const modalBody = document.getElementById('mermaid-modal-body')!; + const modalContent = document.getElementById('mermaid-modal-content')!; + let pzInstance: any = null; + let panzoom: any = null; + + const loadPanzoom = async () => { + if (!panzoom) { + const url = PANZOOM_CDN; + panzoom = (await import(url)).default; + } + return panzoom; + }; + + const fitToScreen = () => { + const wrapper = modalContent.querySelector('.mermaid-panzoom-container') as HTMLElement | null; + if (!pzInstance || !wrapper) return; + const w = +(wrapper.dataset.nativeWidth ?? 0); + const h = +(wrapper.dataset.nativeHeight ?? 0); + const rect = modalContent.getBoundingClientRect(); + const scale = Math.min((rect.width - 60) / w, (rect.height - 60) / h); + pzInstance.zoomAbs(0, 0, scale); + pzInstance.moveTo((rect.width - w * scale) / 2, (rect.height - h * scale) / 2); + }; + + const closeModal = () => { + modal.classList.remove('active'); + document.body.style.overflow = ''; + pzInstance?.dispose(); + pzInstance = null; + modalContent.innerHTML = ''; + }; + + const openModal = async (idx: number) => { + const svg = elements[idx].querySelector('svg'); + if (!svg) return; + + const svgClone = svg.cloneNode(true) as SVGElement; + const viewBox = svg.getAttribute('viewBox'); + const [w, h] = viewBox + ? viewBox.split(/[\s,]+/).slice(2).map(Number) + : [svg.getBoundingClientRect().width || 800, svg.getBoundingClientRect().height || 600]; + svgClone.setAttribute('width', String(w)); + svgClone.setAttribute('height', String(h)); + + const wrapper = document.createElement('div'); + wrapper.className = 'mermaid-panzoom-container'; + wrapper.dataset.nativeWidth = String(w); + wrapper.dataset.nativeHeight = String(h); + wrapper.appendChild(svgClone); + + modalContent.innerHTML = ''; + modalContent.appendChild(wrapper); + modal.classList.add('active'); + document.body.style.overflow = 'hidden'; + + const pz = await loadPanzoom(); + setTimeout(() => { + pzInstance = pz(wrapper, { maxZoom: 10, minZoom: 0.05, bounds: false }); + fitToScreen(); + wrapper.classList.add('ready'); + }, 50); + }; + + // Event delegation + document.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + const toolbarBtn = target.closest('.mermaid-toolbar button') as HTMLElement | null; + if (toolbarBtn) return openModal(+(toolbarBtn.dataset.idx!)); + + const zoomBtn = target.closest('.mermaid-modal-controls button') as HTMLElement | null; + if (zoomBtn && pzInstance) { + const z = zoomBtn.dataset.zoom; + const rect = modalBody.getBoundingClientRect(); + if (z === 'fit') fitToScreen(); + else if (z === '0') { pzInstance.moveTo(0, 0); pzInstance.zoomAbs(0, 0, 1); } + else pzInstance.smoothZoom(rect.width / 2, rect.height / 2, z === '1' ? 1.5 : 0.67); + } + }); + + document.getElementById('mermaid-modal-close')!.addEventListener('click', closeModal); + modalBody.addEventListener('click', (e) => { if (e.target === modalBody) closeModal(); }); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modal.classList.contains('active')) closeModal(); + }); +} + +export async function initMermaidPage(config: MermaidConfig) { + const elements = document.querySelectorAll('.mermaid') as NodeListOf; + if (!elements.length) return; + + const sources = Array.from(elements).map(el => el.innerHTML); + const perDiagramTransparent = sources.map(src => /%%\s*transparent\s*%%/i.test(src)); + const cache: Record = { light: [], dark: [] }; + + const themes = { + light: buildThemeConfig(config, 'light'), + dark: buildThemeConfig(config, 'dark'), + }; + const baseConfig = buildBaseConfig(config); + + const applyTransparency = (el: HTMLElement, i: number) => { + if (perDiagramTransparent[i]) el.querySelector('svg')?.style.setProperty('background', 'transparent'); + }; + + setupWrappers(elements); + setupModal(elements); + + // Initial render + const scheme = getScheme(); + initWithTheme(scheme, themes, baseConfig); + await mermaid.run({ nodes: Array.from(elements) }); + elements.forEach((el, i) => { + el.style.visibility = ''; + cache[scheme][i] = el.innerHTML; + applyTransparency(el, i); + }); + + // Pre-render alternate theme during idle time + const alt: Scheme = scheme === 'dark' ? 'light' : 'dark'; + const idle = window.requestIdleCallback ?? ((fn: IdleRequestCallback) => setTimeout(fn, 1000)); + idle(() => { + if (cache[alt].length) return; + initWithTheme(alt, themes, baseConfig); + renderOffscreen(sources).then(results => { cache[alt] = results; }); + }); + + // Swap cached diagrams on theme change + window.addEventListener('onColorSchemeChange', async () => { + const newScheme = getScheme(); + if (!cache[newScheme].length) { + initWithTheme(newScheme, themes, baseConfig); + cache[newScheme] = await renderOffscreen(sources); + } + elements.forEach((el, i) => { el.innerHTML = cache[newScheme][i]; applyTransparency(el, i); }); + }); +} diff --git a/themes/stack/assets/ts/pagination.ts b/themes/stack/assets/ts/pagination.ts new file mode 100644 index 0000000..aa93f9f --- /dev/null +++ b/themes/stack/assets/ts/pagination.ts @@ -0,0 +1,104 @@ +export function setupPaginationJump() { + const triggers = document.querySelectorAll('.pagination-jump-trigger'); + const dialog = document.getElementById('pagination-jump-dialog') as HTMLDialogElement; + + if (!dialog || triggers.length === 0) return; + + const nav = document.querySelector('.pagination') as HTMLElement; + const input = document.getElementById('pagination-jump-input') as HTMLInputElement; + const form = dialog.querySelector('.pagination-jump-form') as HTMLFormElement; + const supportsDialog = typeof dialog.showModal === 'function' && typeof dialog.close === 'function'; + let lastFocusedElement: HTMLElement | null = null; + + if (!supportsDialog || !nav || !input || !form) return; + + const closeDialog = (): void => { + if (dialog.classList.contains('closing')) return; + dialog.classList.add('closing'); + dialog.addEventListener( + 'animationend', + () => { + dialog.classList.remove('closing'); + dialog.close(); + if (lastFocusedElement?.isConnected) { + lastFocusedElement.focus(); + } + }, + { once: true } + ); + }; + + // Open dialog when triggers are clicked + triggers.forEach((trigger) => { + trigger.addEventListener('click', () => { + const activeElement = document.activeElement; + lastFocusedElement = activeElement instanceof HTMLElement ? activeElement : trigger; + dialog.showModal(); + input.value = ''; + input.focus(); + }); + }); + + // Handle ESC key closing the dialog + dialog.addEventListener('cancel', (e) => { + e.preventDefault(); + closeDialog(); + }); + + // Close dialog when clicking backdrop + dialog.addEventListener('click', (e) => { + const rect = dialog.getBoundingClientRect(); + const isInDialog = + rect.top <= e.clientY && + e.clientY <= rect.top + rect.height && + rect.left <= e.clientX && + e.clientX <= rect.left + rect.width; + if (!isInDialog) { + closeDialog(); + } + }); + + // Allow Enter key to trigger validation and submit + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (form.reportValidity()) { + form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true })); + } + } + }); + + // Handle form submission + form.addEventListener('submit', (e) => { + e.preventDefault(); + + if (!form.checkValidity()) { + form.reportValidity(); + return; + } + + const targetPage = parseInt(input.value); + if (isNaN(targetPage) || targetPage < 1) return; + + const totalPages = parseInt(nav.dataset.total || '0'); + if (targetPage > totalPages) return; + + const firstUrl = nav.dataset.firstUrl || ''; + const formatUrl = nav.dataset.formatUrl || ''; + + let targetUrl = ''; + if (targetPage === 1) { + targetUrl = firstUrl; + } else { + // formatUrl is the URL for page 2. E.g., /tags/page/2/ or /page/2/ + // Replace the '2' before the trailing slash or .html with the target page number + targetUrl = formatUrl.replace(/2([^\d]*)$/, `${targetPage}$1`); + } + + if (targetUrl) { + window.location.href = targetUrl; + } + + closeDialog(); + }); +} diff --git a/themes/stack/assets/ts/scrollspy.ts b/themes/stack/assets/ts/scrollspy.ts new file mode 100644 index 0000000..7cf416e --- /dev/null +++ b/themes/stack/assets/ts/scrollspy.ts @@ -0,0 +1,140 @@ +// Implements a scroll spy system for the ToC, displaying the current section with an indicator and scrolling to it when needed. + +// Inspired from https://gomakethings.com/debouncing-your-javascript-events/ +function debounced(func: Function) { + let timeout; + return () => { + if (timeout) { + window.cancelAnimationFrame(timeout); + } + + timeout = window.requestAnimationFrame(() => func()); + } +} + +const headersQuery = ".article-content h1[id], .article-content h2[id], .article-content h3[id], .article-content h4[id], .article-content h5[id], .article-content h6[id]"; +const tocQuery = "#TableOfContents"; +const navigationQuery = "#TableOfContents li"; +const activeClass = "active-class"; + +function scrollToTocElement(tocElement: HTMLElement, scrollableNavigation: HTMLElement) { + let textHeight = tocElement.querySelector("a").offsetHeight; + let scrollTop = tocElement.offsetTop - scrollableNavigation.offsetHeight / 2 + textHeight / 2 - scrollableNavigation.offsetTop; + if (scrollTop < 0) { + scrollTop = 0; + } + scrollableNavigation.scrollTo({ top: scrollTop, behavior: "smooth" }); +} + +type IdToElementMap = { [key: string]: HTMLElement }; + +function buildIdToNavigationElementMap(navigation: NodeListOf): IdToElementMap { + const sectionLinkRef: IdToElementMap = {}; + navigation.forEach((navigationElement: HTMLElement) => { + const link = navigationElement.querySelector("a"); + if (link) { + const href = link.getAttribute("href"); + if (href.startsWith("#")) { + sectionLinkRef[href.slice(1)] = navigationElement; + } + } + }); + + return sectionLinkRef; +} + +function computeOffsets(headers: NodeListOf) { + let sectionsOffsets = []; + headers.forEach((header: HTMLElement) => { sectionsOffsets.push({ id: header.id, offset: header.offsetTop }) }); + sectionsOffsets.sort((a, b) => a.offset - b.offset); + return sectionsOffsets; +} + +function setupScrollspy() { + let headers = document.querySelectorAll(headersQuery); + if (!headers) { + console.warn("No header matched query", headers); + return; + } + + let scrollableNavigation = document.querySelector(tocQuery) as HTMLElement | undefined; + if (!scrollableNavigation) { + console.warn("No toc matched query", tocQuery); + return; + } + + let navigation = document.querySelectorAll(navigationQuery); + if (!navigation) { + console.warn("No navigation matched query", navigationQuery); + return; + } + + let sectionsOffsets = computeOffsets(headers); + + // We need to avoid scrolling when the user is actively interacting with the ToC. Otherwise, if the user clicks on a link in the ToC, + // we would scroll their view, which is not optimal usability-wise. + let tocHovered: boolean = false; + scrollableNavigation.addEventListener("mouseenter", debounced(() => tocHovered = true)); + scrollableNavigation.addEventListener("mouseleave", debounced(() => tocHovered = false)); + + let activeSectionLink: Element; + + let idToNavigationElement: IdToElementMap = buildIdToNavigationElementMap(navigation); + + function scrollHandler() { + let scrollPosition = document.documentElement.scrollTop || document.body.scrollTop; + + let newActiveSection: HTMLElement | undefined; + + // Find the section that is currently active. + // It is possible for no section to be active, so newActiveSection may be undefined. + sectionsOffsets.forEach((section) => { + if (scrollPosition >= section.offset - 20) { + newActiveSection = document.getElementById(section.id); + } + }); + + // Find the link for the active section. Once again, there are a few edge cases: + // - No active section = no link => undefined + // - No active section but the link does not exist in toc (e.g. because it is outside of the applicable ToC levels) => undefined + let newActiveSectionLink: HTMLElement | undefined + if (newActiveSection) { + newActiveSectionLink = idToNavigationElement[newActiveSection.id]; + } + + if (newActiveSection && !newActiveSectionLink) { + // The active section does not have a link in the ToC, so we can't scroll to it. + console.debug("No link found for section", newActiveSection); + } else if (newActiveSectionLink !== activeSectionLink) { + if (activeSectionLink) + activeSectionLink.classList.remove(activeClass); + if (newActiveSectionLink) { + newActiveSectionLink.classList.add(activeClass); + if (!tocHovered) { + // Scroll so that newActiveSectionLink is in the middle of scrollableNavigation, except when it's from a manual click (hence the tocHovered check) + scrollToTocElement(newActiveSectionLink, scrollableNavigation); + } + } + activeSectionLink = newActiveSectionLink; + } + } + + window.addEventListener("scroll", debounced(scrollHandler)); + + // Resizing may cause the offset values to change: recompute them. + function resizeHandler() { + sectionsOffsets = computeOffsets(headers); + scrollHandler(); + } + + // Use ResizeObserver to detect changes in the size of .article-content + const articleContent = document.querySelector(".article-content"); + if (articleContent) { + const resizeObserver = new ResizeObserver(debounced(resizeHandler)); + resizeObserver.observe(articleContent); + } + + window.addEventListener("resize", debounced(resizeHandler)); +} + +export { setupScrollspy }; diff --git a/themes/stack/assets/ts/search.tsx b/themes/stack/assets/ts/search.tsx new file mode 100644 index 0000000..e932129 --- /dev/null +++ b/themes/stack/assets/ts/search.tsx @@ -0,0 +1,348 @@ +interface pageData { + title: string, + date: string, + permalink: string, + content: string, + image?: string, + preview: string, + matchCount: number +} + +interface match { + start: number, + end: number +} + +/** + * Escape HTML tags as HTML entities + * Edited from: + * @link https://stackoverflow.com/a/5499821 + */ +const tagsToReplace = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '…': '…' +}; + +function replaceTag(tag: string) { + return (tagsToReplace as Record)[tag] || tag; +} + +function replaceHTMLEnt(str: string) { + return str.replace(/[&<>"]/g, replaceTag); +} + +function escapeRegExp(string: string) { + return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); +} + +class Search { + private data: pageData[]; + private form: HTMLFormElement; + private input: HTMLInputElement; + private list: HTMLDivElement; + private resultTitle: HTMLHeadElement; + private resultTitleTemplate: string; + private container: HTMLDivElement; + + constructor({ form, input, list, resultTitle, resultTitleTemplate }: { + form: HTMLFormElement, + input: HTMLInputElement, + list: HTMLDivElement, + resultTitle: HTMLHeadingElement, + resultTitleTemplate: string + }) { + this.form = form; + this.input = input; + this.list = list; + this.resultTitle = resultTitle; + this.resultTitleTemplate = resultTitleTemplate; + this.container = list.parentElement as HTMLDivElement; + + /// Check if there's already value in the search input + if (this.input.value.trim() !== '') { + this.doSearch(this.input.value.split(' ')); + } + else { + this.handleQueryString(); + } + + this.bindQueryStringChange(); + this.bindSearchForm(); + } + + /** + * Processes search matches + * @param str original text + * @param matches array of matches + * @param ellipsis whether to add ellipsis to the end of each match + * @param charLimit max length of preview string + * @param offset how many characters before and after the match to include in preview + * @returns preview string + */ + private static processMatches(str: string, matches: match[], ellipsis: boolean = true, charLimit = 140, offset = 20): string { + matches.sort((a, b) => { + return a.start - b.start; + }); + + let i = 0, + lastIndex = 0, + charCount = 0; + + const resultArray: string[] = []; + + while (i < matches.length) { + const item = matches[i]; + + /// item.start >= lastIndex (equal only for the first iteration) + /// because of the while loop that comes after, iterating over variable j + + if (ellipsis && item.start - offset > lastIndex) { + resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, lastIndex + offset))} [...] `); + resultArray.push(`${replaceHTMLEnt(str.substring(item.start - offset, item.start))}`); + charCount += offset * 2; + } + else { + /// If the match is too close to the end of last match, don't add ellipsis + resultArray.push(replaceHTMLEnt(str.substring(lastIndex, item.start))); + charCount += item.start - lastIndex; + } + + let j = i + 1, + end = item.end; + + /// Include as many matches as possible + /// [item.start, end] is the range of the match + while (j < matches.length && matches[j].start <= end) { + end = Math.max(matches[j].end, end); + ++j; + } + + resultArray.push(`${replaceHTMLEnt(str.substring(item.start, end))}`); + charCount += end - item.start; + + i = j; + lastIndex = end; + + if (ellipsis && charCount > charLimit) break; + } + + /// Add the rest of the string + if (lastIndex < str.length) { + let end = str.length; + if (ellipsis) end = Math.min(end, lastIndex + offset); + + resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, end))}`); + + if (ellipsis && end != str.length) { + resultArray.push(` [...]`); + } + } + + return resultArray.join(''); + } + + private async searchKeywords(keywords: string[]) { + const rawData = await this.getData(); + const results: pageData[] = []; + + const regex = new RegExp(keywords.filter((v, index, arr) => { + arr[index] = escapeRegExp(v); + return v.trim() !== ''; + }).join('|'), 'gi'); + + for (const item of rawData) { + const titleMatches: match[] = [], + contentMatches: match[] = []; + + let result = { + ...item, + preview: '', + matchCount: 0 + } + + const contentMatchAll = item.content.matchAll(regex); + for (const match of Array.from(contentMatchAll)) { + contentMatches.push({ + start: match.index, + end: match.index + match[0].length + }); + } + + const titleMatchAll = item.title.matchAll(regex); + for (const match of Array.from(titleMatchAll)) { + titleMatches.push({ + start: match.index, + end: match.index + match[0].length + }); + } + + if (titleMatches.length > 0) result.title = Search.processMatches(result.title, titleMatches, false); + if (contentMatches.length > 0) { + result.preview = Search.processMatches(result.content, contentMatches); + } + else { + /// If there are no matches in the content, use the first 140 characters as preview + result.preview = replaceHTMLEnt(result.content.substring(0, 140)); + } + + result.matchCount = titleMatches.length + contentMatches.length; + if (result.matchCount > 0) results.push(result); + } + + /// Result with more matches appears first + return results.sort((a, b) => { + return b.matchCount - a.matchCount; + }); + } + + private async doSearch(keywords: string[]) { + const startTime = performance.now(); + + const results = await this.searchKeywords(keywords); + this.clear(); + + for (const item of results) { + this.list.append(Search.render(item)); + } + + const endTime = performance.now(); + + this.resultTitle.innerText = this.generateResultTitle(results.length, ((endTime - startTime) / 1000).toPrecision(1)); + + this.container?.classList.remove('hidden'); + } + + private generateResultTitle(resultLen: number, time: string) { + return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen.toString()).replace("#TIME_SECONDS", time); + } + + public async getData() { + if (!this.data) { + /// Not fetched yet + const jsonURL = this.form.dataset.json as string; + this.data = await fetch(jsonURL).then(res => res.json()); + const parser = new DOMParser(); + + for (const item of this.data) { + item.content = parser.parseFromString(item.content, 'text/html').body.innerText; + } + } + + return this.data; + } + + private bindSearchForm() { + let lastSearch = ''; + + const eventHandler = (e: Event) => { + e.preventDefault(); + const keywords = this.input.value.trim(); + + Search.updateQueryString(keywords, true); + + if (keywords === '') { + lastSearch = ''; + return this.clear(); + } + + if (lastSearch === keywords) return; + lastSearch = keywords; + + this.doSearch(keywords.split(' ')); + } + + this.input.addEventListener('input', eventHandler); + this.input.addEventListener('compositionend', eventHandler); + } + + private clear() { + this.list.innerHTML = ''; + this.resultTitle.innerText = ''; + this.container.classList.add('hidden'); + } + + private bindQueryStringChange() { + window.addEventListener('popstate', (e) => { + this.handleQueryString() + }) + } + + private handleQueryString() { + const pageURL = new URL(window.location.toString()); + const keywords = pageURL.searchParams.get('keyword') || ''; + this.input.value = keywords; + + if (keywords) { + this.doSearch(keywords.split(' ')); + } + else { + this.clear() + } + } + + private static updateQueryString(keywords: string, replaceState = false) { + const pageURL = new URL(window.location.toString()); + + if (keywords === '') { + pageURL.searchParams.delete('keyword') + } + else { + pageURL.searchParams.set('keyword', keywords); + } + + if (replaceState) { + window.history.replaceState('', '', pageURL.toString()); + } + else { + window.history.pushState('', '', pageURL.toString()); + } + } + + public static render(item: pageData) { + return ( +
+ ); + } +} + +declare global { + interface Window { + searchResultTitleTemplate: string; + } +} + +window.addEventListener('load', () => { + setTimeout(function () { + const searchForm = document.querySelector('.search-form') as HTMLFormElement, + searchInput = searchForm?.querySelector('input') as HTMLInputElement, + searchResultList = document.querySelector('.search-result--list') as HTMLDivElement, + searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement; + + if (!searchForm || !searchInput || !searchResultList || !searchResultTitle) return; + + new Search({ + form: searchForm, + input: searchInput, + list: searchResultList, + resultTitle: searchResultTitle, + resultTitleTemplate: window.searchResultTitleTemplate + }); + }, 0); +}) + +export default Search; \ No newline at end of file diff --git a/themes/stack/assets/ts/smoothAnchors.ts b/themes/stack/assets/ts/smoothAnchors.ts new file mode 100644 index 0000000..16ab6a3 --- /dev/null +++ b/themes/stack/assets/ts/smoothAnchors.ts @@ -0,0 +1,37 @@ +// Implements smooth scrolling when clicking on an anchor link. +// This is required instead of using modern CSS because Chromium does not currently support scrolling +// one element with scrollTo while another element is scrolled because of a click on a link. This would +// thus not work with the ToC scrollspy and e.g. footnotes. + +// Here are additional links about this issue: +// - https://stackoverflow.com/questions/49318497/google-chrome-simultaneously-smooth-scrollintoview-with-more-elements-doesn +// - https://stackoverflow.com/questions/57214373/scrollintoview-using-smooth-function-on-multiple-elements-in-chrome +// - https://bugs.chromium.org/p/chromium/issues/detail?id=833617 +// - https://bugs.chromium.org/p/chromium/issues/detail?id=1043933 +// - https://bugs.chromium.org/p/chromium/issues/detail?id=1121151 + +const anchorLinksQuery = "a[href]"; + +function setupSmoothAnchors() { + document.querySelectorAll(anchorLinksQuery).forEach(aElement => { + let href = aElement.getAttribute("href"); + if (!href.startsWith("#")) { + return; + } + aElement.addEventListener("click", clickEvent => { + clickEvent.preventDefault(); + + const targetId = decodeURI(aElement.getAttribute("href").substring(1)), + target = document.getElementById(targetId) as HTMLElement, + offset = target.getBoundingClientRect().top - document.documentElement.getBoundingClientRect().top; + + window.history.pushState({}, "", aElement.getAttribute("href")); + scrollTo({ + top: offset, + behavior: "smooth" + }); + }); + }); +} + +export { setupSmoothAnchors }; \ No newline at end of file diff --git a/themes/stack/config/_default/module.toml b/themes/stack/config/_default/module.toml new file mode 100644 index 0000000..3e283b5 --- /dev/null +++ b/themes/stack/config/_default/module.toml @@ -0,0 +1,3 @@ +[hugoVersion] + extended = true + min = "0.157.0" diff --git a/themes/stack/config/_default/params.toml b/themes/stack/config/_default/params.toml new file mode 100644 index 0000000..c077110 --- /dev/null +++ b/themes/stack/config/_default/params.toml @@ -0,0 +1,202 @@ +mainSections = ["post"] +rssFullContent = true +# Accepted values: "default", "lastmod" +# default = see https://gohugo.io/quick-reference/glossary/#default-sort-order +# lastmod = sort by last modified date, in descending order +SortBy = "default" + +[footer] + since = 2026 + customText = "" + +[dateFormat] + published = ":date_full" + lastUpdated = ":date_full" + +[sidebar] + compact = false + avatar = "" + +[article] + headingAnchor = false + math = false + toc = true + readingTime = true + + [article.list] + # Showing article tags in the list view (e.g. homepage, section pages) + showTags = false + + [article.license] + enabled = false + default = "Licensed under CC BY-NC-SA 4.0" + + # Mermaid diagram configuration + # Diagrams are only loaded when mermaid code blocks are present + # Theme auto-switches based on site light/dark mode + [article.mermaid] + # Visual style: classic or handDrawn (sketch style) + look = "classic" + # Theme for light mode: default, neutral, dark, forest, base, null + lightTheme = "default" + # Theme for dark mode: default, neutral, dark, forest, base, null + darkTheme = "dark" + # Custom theme variables for light mode (only works with lightTheme: base) + # lightThemeVariables = { primaryColor = "#ff0000" } + # Custom theme variables for dark mode (only works with darkTheme: base) + # darkThemeVariables = { primaryColor = "#00ff00" } + # Security level: strict (default), loose, antiscript, sandbox + # Set to "loose" to enable HTML labels like
+ securityLevel = "strict" + # Enable HTML labels in diagrams (requires securityLevel: loose) + htmlLabels = true + # Make diagram backgrounds transparent (default: false) + # Can also be set per-diagram with %%transparent%% directive + transparentBackground = false + + [article.alertIcon] + # Alert icons for different alert types (note, tip, important, warning, caution) + note = "📝" + tip = "💡" + important = "📌" + warning = "⚠️" + caution = "🚨" + +[widgets] + homepage = [] + page = [] + +[opengraph.twitter] + site = "" + card = "summary_large_image" + +[colorScheme] + toggle = true + default = "auto" + +[imageProcessing] + autoOrient = false + + [imageProcessing.external] + timeout = "5s" + + [imageProcessing.content] + widths = [800, 1600, 2400] + enabled = true + + [imageProcessing.thumbnail] + enabled = true + +# GDPR Cookie Consent Configuration +# When enabled, analytics and functional cookies require user consent +[cookies] + enabled = false + showSettings = true + + [cookies.categories] + analytics = true + functional = true + +[comments] + enabled = true + provider = "disqus" + + [comments.artalk] + serverUrl = "" + site = "" + + [comments.disqusjs] + shortname = "" + apiUrl = "" + apiKey = "" + admin = "" + adminLabel = "" + + [comments.utterances] + repo = "" + issueTerm = "pathname" + label = "" + + [comments.beaudar] + repo = "" + issueTerm = "pathname" + label = "" + theme = "" + + [comments.remark42] + host = "" + site = "" + locale = "en" + max_shown_comments = 15 + show_email_subscription = true + + [comments.vssue] + platform = "" + owner = "" + repo = "" + clientId = "" + clientSecret = "" + autoCreateIssue = false + + # Waline client configuration see: https://waline.js.org/en/reference/component.html + [comments.waline] + serverURL = "" + lang = "" + pageview = "" + emoji = ["https://unpkg.com/@waline/emojis@1.0.1/weibo"] + requiredMeta = ["name", "email", "url"] + + [comments.waline.locale] + admin = "Admin" + placeholder = "" + + [comments.twikoo] + envId = "" + region = "" + path = "" + lang = "" + + # See https://cactus.chat/docs/reference/web-client/#configuration for description of the various options + [comments.cactus] + defaultHomeserverUrl = "https://matrix.cactus.chat:8448" + serverName = "cactus.chat" + siteName = "" # You must insert a unique identifier here matching the one you registered (See https://cactus.chat/docs/getting-started/quick-start/#register-your-site) + + [comments.giscus] + repo = "" + repoID = "" + category = "" + categoryID = "" + mapping = "title" + lightTheme = "light" + darkTheme = "dark_dimmed" + reactionsEnabled = 1 + emitMetadata = 0 + inputPosition = "top" + lang = "en" + strict = 0 + loading = "lazy" + + [comments.gitalk] + owner = "" + admin = "" + repo = "" + clientID = "" + clientSecret = "" + proxy = "" + + [comments.cusdis] + host = "https://cusdis.com" + id = "" + + # Comentario client configuration + [comments.comentario] + url = "" + autoInit = false + autoNonInteractiveSso = false + cssOverride = "" + lang = "" + liveUpdate = false + maxLevel = 0 + noFonts = false + theme = "" diff --git a/themes/stack/data/external.toml b/themes/stack/data/external.toml new file mode 100644 index 0000000..5a95329 --- /dev/null +++ b/themes/stack/data/external.toml @@ -0,0 +1,19 @@ +KaTeX = [ + { src = "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css", integrity = "sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV", type = "style" }, + { src = "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js", integrity = "sha384-XjKyOOlGwcjNTAIQHIpgOno0Hl1YQqzUOEleOLALmuqehneUG+vnGctmUb0ZY0l8", type = "script", defer = true }, + { src = "https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js", integrity = "sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05", type = "script", defer = true }, +] + +Cactus = [ + { src = "https://latest.cactus.chat/cactus.js", type = "script" }, + { src = "https://latest.cactus.chat/style.css", type = "style" }, +] + +[PhotoSwipe] + Style = "https://cdn.jsdelivr.net/npm/photoswipe@5.4.4/dist/photoswipe.css" + Core = "https://cdn.jsdelivr.net/npm/photoswipe@5.4.4/dist/photoswipe.esm.min.js" + Lightbox = "https://cdn.jsdelivr.net/npm/photoswipe@5.4.4/dist/photoswipe-lightbox.esm.min.js" + +[Artalk] + Style = "https://cdn.jsdelivr.net/npm/artalk@2.9.1/dist/Artalk.css" + Script = "https://cdn.jsdelivr.net/npm/artalk@2.9.1/+esm" diff --git a/themes/stack/demo/.gitignore b/themes/stack/demo/.gitignore new file mode 100644 index 0000000..6db5459 --- /dev/null +++ b/themes/stack/demo/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test + +public +themes +resources + +assets/jsconfig.json +.DS_Store diff --git a/themes/stack/demo/LICENSE b/themes/stack/demo/LICENSE new file mode 100644 index 0000000..4527efb --- /dev/null +++ b/themes/stack/demo/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Steve Francia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/themes/stack/demo/README.md b/themes/stack/demo/README.md new file mode 100644 index 0000000..dbaf5e5 --- /dev/null +++ b/themes/stack/demo/README.md @@ -0,0 +1,3 @@ +This is an example site used for solely for testing purposes. **It is not meant to be used as a template for your site**. + +To create a new site, please use the starter template: [CaiJimmy/hugo-theme-stack-starter](https://github.com/CaiJimmy/hugo-theme-stack-starter/) \ No newline at end of file diff --git a/themes/stack/demo/assets/img/avatar.png b/themes/stack/demo/assets/img/avatar.png new file mode 100644 index 0000000..d90352c Binary files /dev/null and b/themes/stack/demo/assets/img/avatar.png differ diff --git a/themes/stack/demo/config/_default/hugo.toml b/themes/stack/demo/config/_default/hugo.toml new file mode 100644 index 0000000..646da2a --- /dev/null +++ b/themes/stack/demo/config/_default/hugo.toml @@ -0,0 +1,22 @@ +baseURL = "https://demo.stack.cai.im/" +locale = "en-us" +title = "Hugo Theme Stack" +# Theme i18n support +# See i18n folder for available languages +defaultContentLanguage = "en" +# Set hasCJKLanguage to true if DefaultContentLanguage is a CJK language +# This will make .Summary and .WordCount behave correctly +hasCJKLanguage = false + +[[module.imports]] + path = "github.com/CaiJimmy/hugo-theme-stack/v3" + +[pagination] + pagerSize = 3 + +[permalinks] + post = "/p/:slug/" + page = "/:slug/" + +[services.disqus] + shortname = "hugo-theme-stack" diff --git a/themes/stack/demo/config/_default/languages.toml b/themes/stack/demo/config/_default/languages.toml new file mode 100644 index 0000000..bba9268 --- /dev/null +++ b/themes/stack/demo/config/_default/languages.toml @@ -0,0 +1,31 @@ +[en] + label = "English" + title = "Hugo Theme Stack" + weight = 1 + + [en.params.sidebar] + subtitle = "Card-style Hugo theme designed for bloggers" + +[zh] + label = "简体中文" + title = "Hugo 主题 Stack" + weight = 2 + + [zh.params.sidebar] + subtitle = "为博客设计的卡片式 Hugo 主题" + +[zh-hant-tw] + label = "正體中文" + title = "Hugo 主題 Stack" + weight = 3 + + [zh-hant-tw.params.sidebar] + subtitle = "為博客設計的卡片式 Hugo 主題" + +[ja] + label = "日本語" + title = "Hugo Theme Stack" + weight = 4 + + [ja.params.sidebar] + subtitle = "ブログ用のカードスタイルの Hugo テーマ" diff --git a/themes/stack/demo/config/_default/markup.toml b/themes/stack/demo/config/_default/markup.toml new file mode 100644 index 0000000..3b38cfe --- /dev/null +++ b/themes/stack/demo/config/_default/markup.toml @@ -0,0 +1,26 @@ +[goldmark] + [goldmark.extensions] + [goldmark.extensions.passthrough] + enable = true + + [goldmark.extensions.passthrough.delimiters] + block = [["\\[", "\\]"], ["$$", "$$"]] + inline = [["\\(", "\\)"]] + + [goldmark.renderer] + ## Set to true if you have HTML content inside Markdown + unsafe = true + +[tableOfContents] + endLevel = 4 + ordered = true + startLevel = 2 + +[highlight] + noClasses = false + codeFences = true + guessSyntax = true + lineNoStart = 1 + lineNos = true + lineNumbersInTable = true + tabWidth = 4 diff --git a/themes/stack/demo/config/_default/menu.toml b/themes/stack/demo/config/_default/menu.toml new file mode 100644 index 0000000..f61190a --- /dev/null +++ b/themes/stack/demo/config/_default/menu.toml @@ -0,0 +1,15 @@ +[[social]] + identifier = "github" + name = "GitHub" + url = "https://github.com/CaiJimmy/hugo-theme-stack" + + [social.params] + icon = "brand-github" + +[[social]] + identifier = "twitter" + name = "Twitter" + url = "https://twitter.com" + + [social.params] + icon = "brand-twitter" diff --git a/themes/stack/demo/config/_default/params.toml b/themes/stack/demo/config/_default/params.toml new file mode 100644 index 0000000..9cd5956 --- /dev/null +++ b/themes/stack/demo/config/_default/params.toml @@ -0,0 +1,39 @@ +mainSections = ["post"] +rssFullContent = true +favicon = "img/avatar.png" + +[footer] + since = 2020 + +[sidebar] + emoji = "✏️" + subtitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + avatar = "img/avatar.png" + +[article] + [article.license] + enabled = true + default = "Licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)" + +[widgets] + homepage = [ + { type = "search" }, + { type = "archives", params = { limit = 5 } }, + { type = "categories", params = { limit = 10 } }, + { type = "tag-cloud", params = { limit = 10 } }, + ] + page = [{ type = "toc" }] + +[comments] + # See complete configuration in config/_default/params.toml + enabled = true + provider = "disqus" + +# GDPR Cookie Consent Configuration +# When enabled, analytics and functional cookies require user consent +[cookies] + enabled = false + + [cookies.categories] + analytics = true + functional = true diff --git a/themes/stack/demo/config/_default/related.toml b/themes/stack/demo/config/_default/related.toml new file mode 100644 index 0000000..72b2543 --- /dev/null +++ b/themes/stack/demo/config/_default/related.toml @@ -0,0 +1,7 @@ +includeNewer = true +threshold = 60 +toLower = false +indices = [ + { name = "tags", weight = 100 }, + { name = "categories", weight = 200 }, +] diff --git a/themes/stack/demo/content/_index.ja.md b/themes/stack/demo/content/_index.ja.md new file mode 100644 index 0000000..7b6ff06 --- /dev/null +++ b/themes/stack/demo/content/_index.ja.md @@ -0,0 +1,8 @@ +--- +menu: + main: + name: ホーム + weight: -100 + params: + icon: home +--- \ No newline at end of file diff --git a/themes/stack/demo/content/_index.md b/themes/stack/demo/content/_index.md new file mode 100644 index 0000000..530dfe8 --- /dev/null +++ b/themes/stack/demo/content/_index.md @@ -0,0 +1,8 @@ +--- +menu: + main: + name: Home + weight: -100 + params: + icon: home +--- \ No newline at end of file diff --git a/themes/stack/demo/content/_index.zh-hant-tw.md b/themes/stack/demo/content/_index.zh-hant-tw.md new file mode 100644 index 0000000..65e177b --- /dev/null +++ b/themes/stack/demo/content/_index.zh-hant-tw.md @@ -0,0 +1,8 @@ +--- +menu: + main: + name: 主頁 + weight: -100 + params: + icon: home +--- \ No newline at end of file diff --git a/themes/stack/demo/content/_index.zh.md b/themes/stack/demo/content/_index.zh.md new file mode 100644 index 0000000..e4b59a8 --- /dev/null +++ b/themes/stack/demo/content/_index.zh.md @@ -0,0 +1,8 @@ +--- +menu: + main: + name: 主页 + weight: -100 + params: + icon: home +--- \ No newline at end of file diff --git a/themes/stack/demo/content/categories/Documentation/_index.ja.md b/themes/stack/demo/content/categories/Documentation/_index.ja.md new file mode 100644 index 0000000..4023f13 --- /dev/null +++ b/themes/stack/demo/content/categories/Documentation/_index.ja.md @@ -0,0 +1,9 @@ +--- +title: ドキュメント +description: "Stack テーマの使用方法に関する投稿" +slug: "documentation" +image: "hutomo-abrianto-l2jk-uxb1BY-unsplash.jpg" +style: + background: "#2a9d8f" + color: "#fff" +--- diff --git a/themes/stack/demo/content/categories/Documentation/_index.md b/themes/stack/demo/content/categories/Documentation/_index.md new file mode 100644 index 0000000..2c1e255 --- /dev/null +++ b/themes/stack/demo/content/categories/Documentation/_index.md @@ -0,0 +1,9 @@ +--- +title: Documentation +description: "Posts about how to use Stack theme" +slug: "documentation" +image: "hutomo-abrianto-l2jk-uxb1BY-unsplash.jpg" +style: + background: "#2a9d8f" + color: "#fff" +--- diff --git a/themes/stack/demo/content/categories/Documentation/_index.zh-hant-tw.md b/themes/stack/demo/content/categories/Documentation/_index.zh-hant-tw.md new file mode 100644 index 0000000..40c9edf --- /dev/null +++ b/themes/stack/demo/content/categories/Documentation/_index.zh-hant-tw.md @@ -0,0 +1,9 @@ +--- +title: 文檔 +description: "關於如何使用 Stack 主題的文章" +slug: "documentation" +image: "hutomo-abrianto-l2jk-uxb1BY-unsplash.jpg" +style: + background: "#2a9d8f" + color: "#fff" +--- diff --git a/themes/stack/demo/content/categories/Documentation/_index.zh.md b/themes/stack/demo/content/categories/Documentation/_index.zh.md new file mode 100644 index 0000000..96a69c2 --- /dev/null +++ b/themes/stack/demo/content/categories/Documentation/_index.zh.md @@ -0,0 +1,9 @@ +--- +title: 文档 +description: "关于如何使用 Stack 主题的文章" +slug: "documentation" +image: "hutomo-abrianto-l2jk-uxb1BY-unsplash.jpg" +style: + background: "#2a9d8f" + color: "#fff" +--- diff --git a/themes/stack/demo/content/categories/Documentation/hutomo-abrianto-l2jk-uxb1BY-unsplash.jpg b/themes/stack/demo/content/categories/Documentation/hutomo-abrianto-l2jk-uxb1BY-unsplash.jpg new file mode 100644 index 0000000..c385943 Binary files /dev/null and b/themes/stack/demo/content/categories/Documentation/hutomo-abrianto-l2jk-uxb1BY-unsplash.jpg differ diff --git a/themes/stack/demo/content/page/about/index.ja.md b/themes/stack/demo/content/page/about/index.ja.md new file mode 100644 index 0000000..71e10ca --- /dev/null +++ b/themes/stack/demo/content/page/about/index.ja.md @@ -0,0 +1,46 @@ +--- +title: このサイトについて +description: このサイトとその著者についてのすべて。 +date: 2026-01-26 +lastmod: 2026-01-26 +menu: + main: + weight: -90 + params: + icon: user +--- + +## 誰のためのページですか? + +これは**サンプル詳細**ページです。あなた自身、あなたのブログ、またはあなたのプロジェクトを紹介するために使用できます。 + +## 著者について + +こんにちは!私はブログとオープンソースに情熱を注いでいる愛好家です。新しい技術を探索し、その経験をコミュニティと共有することが大好きです。 + +### 私の歩み + +10年以上前にWeb開発の旅を始めました。それ以来、個人のブログから大規模なエンタープライズアプリケーションまで、数多くのプロジェクトに携わってきました。 + +### スキル + +- **フロントエンド**: HTML, CSS, JavaScript, React, Vue +- **バックエンド**: Node.js, Python, Go +- **ツール**: Git, Docker, Hugo, Neovim + +## お問い合わせ + +質問がある場合や、単にあいさつをしたい場合は、お気軽にご連絡ください! + +- **メール**: [hello@example.com](mailto:hello@example.com) +- **Twitter**: [@example](https://twitter.com/example) +- **GitHub**: [example](https://github.com/example) + +--- + +> "Logic will get you from A to B. Imagination will take you everywhere." +> — *Albert Einstein* + +--- +*このページは AI によって生成されました。* + diff --git a/themes/stack/demo/content/page/about/index.md b/themes/stack/demo/content/page/about/index.md new file mode 100644 index 0000000..8892443 --- /dev/null +++ b/themes/stack/demo/content/page/about/index.md @@ -0,0 +1,46 @@ +--- +title: About +description: Everything you need to know about this site and its author. +date: 2026-01-26 +lastmod: 2026-01-26 +menu: + main: + weight: -90 + params: + icon: user +--- + +## Who is this for? + +This is a sample **About** page. You can use it to introduce yourself, your blog, or your project. + +## About the Author + +Hello! I'm a passionate blogger and open-source enthusiast. I love exploring new technologies and sharing my experiences with the community. + +### My Journey + +I started my journey in web development over 10 years ago. Since then, I've worked on numerous projects ranging from small personal blogs to large-scale enterprise applications. + +### Skills + +- **Front-end**: HTML, CSS, JavaScript, React, Vue +- **Back-end**: Node.js, Python, Go +- **Tools**: Git, Docker, Hugo, Neovim + +## Contact Me + +Feel free to reach out if you have any questions or just want to say hi! + +- **Email**: [hello@example.com](mailto:hello@example.com) +- **Twitter**: [@example](https://twitter.com/example) +- **GitHub**: [example](https://github.com/example) + +--- + +> "Logic will get you from A to B. Imagination will take you everywhere." +> — *Albert Einstein* + +--- +*This page was generated by AI.* + diff --git a/themes/stack/demo/content/page/about/index.zh-hant-tw.md b/themes/stack/demo/content/page/about/index.zh-hant-tw.md new file mode 100644 index 0000000..41d180c --- /dev/null +++ b/themes/stack/demo/content/page/about/index.zh-hant-tw.md @@ -0,0 +1,46 @@ +--- +title: 關於 +description: 關於本站及其作者的一切。 +date: 2026-01-26 +lastmod: 2026-01-26 +menu: + main: + weight: -90 + params: + icon: user +--- + +## 這是給誰看的? + +這是一個示例**關於**頁面。你可以用它來介紹你自己、你的博客或你的項目。 + +## 關於作者 + +你好!我是一個熱衷於博客和開源的愛好者。我喜歡探索新技術並向社區分享我的經驗。 + +### 我的旅程 + +我在 10 多年前開始了我的 Web 開發之旅。自那以後,我參與了許多項目,從個人博客到大規模企業應用。 + +### 技能 + +- **前端**: HTML, CSS, JavaScript, React, Vue +- **後端**: Node.js, Python, Go +- **工具**: Git, Docker, Hugo, Neovim + +## 聯繫我 + +如果你有任何問題或只是想打個招呼,請隨時聯繫我! + +- **電子郵件**: [hello@example.com](mailto:hello@example.com) +- **Twitter**: [@example](https://twitter.com/example) +- **GitHub**: [example](https://github.com/example) + +--- + +> "Logic will get you from A to B. Imagination will take you everywhere." +> — *Albert Einstein* + +--- +*此頁面由 AI 生成。* + diff --git a/themes/stack/demo/content/page/about/index.zh.md b/themes/stack/demo/content/page/about/index.zh.md new file mode 100644 index 0000000..9ac6444 --- /dev/null +++ b/themes/stack/demo/content/page/about/index.zh.md @@ -0,0 +1,45 @@ +--- +title: 关于 +description: 关于本站及其作者的一切。 +date: 2026-01-26 +lastmod: 2026-01-26 +menu: + main: + weight: -90 + params: + icon: user +--- + +## 这是给谁看的? + +这是一个示例**关于**页面。你可以用它来介绍你自己、你的博客或你的项目。 + +## 关于作者 + +你好!我是一个热衷于博客和开源的爱好者。我喜欢探索新技术并向社区分享我的经验。 + +### 我的旅程 + +我在 10 多年前开始了我的 Web 开发之旅。自那以后,我参与了许多项目,从个人博客到大规模企业应用。 + +### 技能 + +- **前端**: HTML, CSS, JavaScript, React, Vue +- **后端**: Node.js, Python, Go +- **工具**: Git, Docker, Hugo, Neovim + +## 联系我 + +如果你有任何问题或只是想打个招呼,请随时联系我! + +- **电子邮件**: [hello@example.com](mailto:hello@example.com) +- **Twitter**: [@example](https://twitter.com/example) +- **GitHub**: [example](https://github.com/example) + +--- + +> "Logic will get you from A to B. Imagination will take you everywhere." +> — *Albert Einstein* + +--- +*此页面由 AI 生成。* \ No newline at end of file diff --git a/themes/stack/demo/content/page/archives/index.ja.md b/themes/stack/demo/content/page/archives/index.ja.md new file mode 100644 index 0000000..2b4faa4 --- /dev/null +++ b/themes/stack/demo/content/page/archives/index.ja.md @@ -0,0 +1,11 @@ +--- +title: "アーカイブ" +date: 2019-05-28 +layout: "archives" +slug: "archives" +menu: + main: + weight: -70 + params: + icon: archives +--- diff --git a/themes/stack/demo/content/page/archives/index.md b/themes/stack/demo/content/page/archives/index.md new file mode 100644 index 0000000..2862deb --- /dev/null +++ b/themes/stack/demo/content/page/archives/index.md @@ -0,0 +1,11 @@ +--- +title: "Archives" +date: 2019-05-28 +layout: "archives" +slug: "archives" +menu: + main: + weight: -70 + params: + icon: archives +--- diff --git a/themes/stack/demo/content/page/archives/index.zh-hant-tw.md b/themes/stack/demo/content/page/archives/index.zh-hant-tw.md new file mode 100644 index 0000000..eebb716 --- /dev/null +++ b/themes/stack/demo/content/page/archives/index.zh-hant-tw.md @@ -0,0 +1,11 @@ +--- +title: "歸檔" +date: 2019-05-28 +layout: "archives" +slug: "archives" +menu: + main: + weight: -70 + params: + icon: archives +--- diff --git a/themes/stack/demo/content/page/archives/index.zh.md b/themes/stack/demo/content/page/archives/index.zh.md new file mode 100644 index 0000000..e3086a7 --- /dev/null +++ b/themes/stack/demo/content/page/archives/index.zh.md @@ -0,0 +1,11 @@ +--- +title: "归档" +date: 2019-05-28 +layout: "archives" +slug: "archives" +menu: + main: + weight: -70 + params: + icon: archives +--- diff --git a/themes/stack/demo/content/page/links/index.ja.md b/themes/stack/demo/content/page/links/index.ja.md new file mode 100644 index 0000000..cf25cb3 --- /dev/null +++ b/themes/stack/demo/content/page/links/index.ja.md @@ -0,0 +1,36 @@ +--- +title: "リンク" +links: + - title: GitHub + description: GitHubは、世界最大のソフトウェア開発プラットフォームです。 + website: https://github.com + image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + - title: TypeScript + description: TypeScriptは、JavaScriptの静的型付けされたスーパーセットで、純粋なJavaScriptにコンパイルされます。 + website: https://www.typescriptlang.org + image: ts-logo-128.jpg +menu: + main: + weight: -50 + params: + icon: link +comments: false +--- + +この機能を使用するには、フロントマターに `links` セクションを追加します。 + +このページのフロントマター: + +```yaml +links: + - title: GitHub + description: GitHubは、世界最大のソフトウェア開発プラットフォームです。 + website: https://github.com + image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + - title: TypeScript + description: TypeScriptは、JavaScriptの静的型付けされたスーパーセットで、純粋なJavaScriptにコンパイルされます。 + website: https://www.typescriptlang.org + image: ts-logo-128.jpg +``` + +`image` フィールドは、ローカル画像と外部画像の両方を受け入れます。 diff --git a/themes/stack/demo/content/page/links/index.md b/themes/stack/demo/content/page/links/index.md new file mode 100644 index 0000000..9e29bf3 --- /dev/null +++ b/themes/stack/demo/content/page/links/index.md @@ -0,0 +1,37 @@ +--- +title: Links +links: + - title: GitHub + description: GitHub is the world's largest software development platform. + website: https://github.com + image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + - title: TypeScript + description: TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. + website: https://www.typescriptlang.org + image: ts-logo-128.jpg +menu: + main: + weight: -50 + params: + icon: link + +comments: false +--- + +To use this feature, add `links` section to frontmatter. + +This page's frontmatter: + +```yaml +links: + - title: GitHub + description: GitHub is the world's largest software development platform. + website: https://github.com + image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + - title: TypeScript + description: TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. + website: https://www.typescriptlang.org + image: ts-logo-128.jpg +``` + +`image` field accepts both local and external images. \ No newline at end of file diff --git a/themes/stack/demo/content/page/links/index.zh-hant-tw.md b/themes/stack/demo/content/page/links/index.zh-hant-tw.md new file mode 100644 index 0000000..0360c49 --- /dev/null +++ b/themes/stack/demo/content/page/links/index.zh-hant-tw.md @@ -0,0 +1,36 @@ +--- +title: "鏈接" +links: + - title: GitHub + description: GitHub 是世界上最大的軟件開發平台。 + website: https://github.com + image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + - title: TypeScript + description: TypeScript 是 JavaScript 的一個超集,它可以編譯成純 JavaScript。 + website: https://www.typescriptlang.org + image: ts-logo-128.jpg +menu: + main: + weight: -50 + params: + icon: link +comments: false +--- + +要使用此功能,請在 frontmatter 中添加 `links` 部分。 + +此頁面的 frontmatter: + +```yaml +links: + - title: GitHub + description: GitHub 是世界上最大的軟件開發平台。 + website: https://github.com + image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + - title: TypeScript + description: TypeScript 是 JavaScript 的一個超集,它可以編譯成純 JavaScript。 + website: https://www.typescriptlang.org + image: ts-logo-128.jpg +``` + +`image` 字段支持本地和外部圖像。 diff --git a/themes/stack/demo/content/page/links/index.zh.md b/themes/stack/demo/content/page/links/index.zh.md new file mode 100644 index 0000000..6532587 --- /dev/null +++ b/themes/stack/demo/content/page/links/index.zh.md @@ -0,0 +1,36 @@ +--- +title: "链接" +links: + - title: GitHub + description: GitHub 是世界上最大的软件开发平台。 + website: https://github.com + image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + - title: TypeScript + description: TypeScript 是 JavaScript 的一个超集,它可以编译成纯 JavaScript。 + website: https://www.typescriptlang.org + image: ts-logo-128.jpg +menu: + main: + weight: -50 + params: + icon: link +comments: false +--- + +要使用此功能,请在 frontmatter 中添加 `links` 部分。 + +此页面的 frontmatter: + +```yaml +links: + - title: GitHub + description: GitHub 是世界上最大的软件开发平台。 + website: https://github.com + image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + - title: TypeScript + description: TypeScript 是 JavaScript 的一个超集,它可以编译成纯 JavaScript。 + website: https://www.typescriptlang.org + image: ts-logo-128.jpg +``` + +`image` 字段支持本地和外部图像。 diff --git a/themes/stack/demo/content/page/links/ts-logo-128.jpg b/themes/stack/demo/content/page/links/ts-logo-128.jpg new file mode 100644 index 0000000..85e3323 Binary files /dev/null and b/themes/stack/demo/content/page/links/ts-logo-128.jpg differ diff --git a/themes/stack/demo/content/page/search/index.ja.md b/themes/stack/demo/content/page/search/index.ja.md new file mode 100644 index 0000000..fdecb37 --- /dev/null +++ b/themes/stack/demo/content/page/search/index.ja.md @@ -0,0 +1,13 @@ +--- +title: "検索" +slug: "search" +layout: "search" +outputs: + - html + - json +menu: + main: + weight: -60 + params: + icon: search +--- diff --git a/themes/stack/demo/content/page/search/index.md b/themes/stack/demo/content/page/search/index.md new file mode 100644 index 0000000..b2a5943 --- /dev/null +++ b/themes/stack/demo/content/page/search/index.md @@ -0,0 +1,13 @@ +--- +title: "Search" +slug: "search" +layout: "search" +outputs: + - html + - json +menu: + main: + weight: -60 + params: + icon: search +--- \ No newline at end of file diff --git a/themes/stack/demo/content/page/search/index.zh-hant-tw.md b/themes/stack/demo/content/page/search/index.zh-hant-tw.md new file mode 100644 index 0000000..6261846 --- /dev/null +++ b/themes/stack/demo/content/page/search/index.zh-hant-tw.md @@ -0,0 +1,13 @@ +--- +title: "搜索" +slug: "search" +layout: "search" +outputs: + - html + - json +menu: + main: + weight: -60 + params: + icon: search +--- diff --git a/themes/stack/demo/content/page/search/index.zh.md b/themes/stack/demo/content/page/search/index.zh.md new file mode 100644 index 0000000..6261846 --- /dev/null +++ b/themes/stack/demo/content/page/search/index.zh.md @@ -0,0 +1,13 @@ +--- +title: "搜索" +slug: "search" +layout: "search" +outputs: + - html + - json +menu: + main: + weight: -60 + params: + icon: search +--- diff --git a/themes/stack/demo/content/post/Image Gallery/florian-klauer-nptLmg6jqDo-unsplash.jpg b/themes/stack/demo/content/post/Image Gallery/florian-klauer-nptLmg6jqDo-unsplash.jpg new file mode 100644 index 0000000..4798ed4 Binary files /dev/null and b/themes/stack/demo/content/post/Image Gallery/florian-klauer-nptLmg6jqDo-unsplash.jpg differ diff --git a/themes/stack/demo/content/post/Image Gallery/helena-hertz-wWZzXlDpMog-unsplash.jpg b/themes/stack/demo/content/post/Image Gallery/helena-hertz-wWZzXlDpMog-unsplash.jpg new file mode 100644 index 0000000..0120706 Binary files /dev/null and b/themes/stack/demo/content/post/Image Gallery/helena-hertz-wWZzXlDpMog-unsplash.jpg differ diff --git a/themes/stack/demo/content/post/Image Gallery/hudai-gayiran-3Od_VKcDEAA-unsplash.jpg b/themes/stack/demo/content/post/Image Gallery/hudai-gayiran-3Od_VKcDEAA-unsplash.jpg new file mode 100644 index 0000000..1a280b2 Binary files /dev/null and b/themes/stack/demo/content/post/Image Gallery/hudai-gayiran-3Od_VKcDEAA-unsplash.jpg differ diff --git a/themes/stack/demo/content/post/Image Gallery/index.ja.md b/themes/stack/demo/content/post/Image Gallery/index.ja.md new file mode 100644 index 0000000..dda26ae --- /dev/null +++ b/themes/stack/demo/content/post/Image Gallery/index.ja.md @@ -0,0 +1,41 @@ +--- +title: 写真ギャラリー +description: 内蔵の写真ギャラリー機能を披露 +date: 2026-01-26 +slug: image-gallery +image: helena-hertz-wWZzXlDpMog-unsplash.jpg +categories: + - Documentation +tags: + - Gallery + - Photoswipe +toc: false +--- + +Stack テーマには、写真ギャラリー機能が内蔵されています。複数の画像を並べて配置するだけで、美しいギャラリーを作成できます。 + +## ギャラリーの例 + +![Photo by Florian Klauer on Unsplash](florian-klauer-nptLmg6jqDo-unsplash.jpg) ![Photo by Luca Bravo on Unsplash](luca-bravo-alS7ewQ41M8-unsplash.jpg) + +![Photo by Helena Hertz on Unsplash](helena-hertz-wWZzXlDpMog-unsplash.jpg) ![Photo by Hudai Gayiran on Unsplash](hudai-gayiran-3Od_VKcDEAA-unsplash.jpg) + +## 仕組み + +ギャラリーは **Photoswipe** と独自の内部スクリプトによって動作します。画像の縦横比に基づいて、最適なレイアウトが自動的に計算されます。 + +ギャラリーを作成するには、同じ行(または同じ段落)に複数の画像を配置するだけです。 + +### 構文 + +```markdown +![画像 1](image1.jpg) ![画像 2](image2.jpg) + +![画像 3](image3.jpg) ![画像 4](image4.jpg) +``` + +> **注意**: Markdown で画像が同じ行に保持されるように、画像の間には 2 つのスペースを入れる必要があります。 + +--- + +ギャラリーの構文は [Typlog](https://typlog.com/) にインスパイアされています。 diff --git a/themes/stack/demo/content/post/Image Gallery/index.md b/themes/stack/demo/content/post/Image Gallery/index.md new file mode 100644 index 0000000..3310c64 --- /dev/null +++ b/themes/stack/demo/content/post/Image Gallery/index.md @@ -0,0 +1,42 @@ +--- +title: Image Gallery +description: Showcasing the built-in image gallery support +date: 2026-01-26 +slug: image-gallery +image: helena-hertz-wWZzXlDpMog-unsplash.jpg +categories: + - Documentation +tags: + - Gallery + - Photoswipe +toc: false +--- + +Stack theme has a built-in support for image galleries. It allows you to create a beautiful gallery by simply placing multiple images side-by-side. + +## Sample Gallery + +![Photo by Florian Klauer on Unsplash](florian-klauer-nptLmg6jqDo-unsplash.jpg) ![Photo by Luca Bravo on Unsplash](luca-bravo-alS7ewQ41M8-unsplash.jpg) + +![Photo by Helena Hertz on Unsplash](helena-hertz-wWZzXlDpMog-unsplash.jpg) ![Photo by Hudai Gayiran on Unsplash](hudai-gayiran-3Od_VKcDEAA-unsplash.jpg) + +## How it works + +The gallery is powered by **Photoswipe** and a custom internal script. It automatically calculates the best layout for your images based on their aspect ratios. + +To create a gallery, you just need to put multiple images in the same line (or paragraph). + +### Syntax + +```markdown +![Image 1](image1.jpg) ![Image 2](image2.jpg) + +![Image 3](image3.jpg) ![Image 4](image4.jpg) +``` + +> **Note**: There should be two spaces between the images to ensure they stay in the same line in Markdown + +--- + +Gallery Syntax inspired by [Typlog](https://typlog.com/) + diff --git a/themes/stack/demo/content/post/Image Gallery/index.zh-hant-tw.md b/themes/stack/demo/content/post/Image Gallery/index.zh-hant-tw.md new file mode 100644 index 0000000..17e60fc --- /dev/null +++ b/themes/stack/demo/content/post/Image Gallery/index.zh-hant-tw.md @@ -0,0 +1,41 @@ +--- +title: 相簿 +description: 展示內置的相簿支持 +date: 2026-01-26 +slug: image-gallery +image: helena-hertz-wWZzXlDpMog-unsplash.jpg +categories: + - Documentation +tags: + - Gallery + - Photoswipe +toc: false +--- + +Stack 主題內置了對相簿的支持。你只需要簡單地將多張圖片並排放置,就可以創建一個精美的相簿。 + +## 示例相簿 + +![Photo by Florian Klauer on Unsplash](florian-klauer-nptLmg6jqDo-unsplash.jpg) ![Photo by Luca Bravo on Unsplash](luca-bravo-alS7ewQ41M8-unsplash.jpg) + +![Photo by Helena Hertz on Unsplash](helena-hertz-wWZzXlDpMog-unsplash.jpg) ![Photo by Hudai Gayiran on Unsplash](hudai-gayiran-3Od_VKcDEAA-unsplash.jpg) + +## 它是如何工作的 + +相簿由 **Photoswipe** 和一個自定義的內部腳本驅動。它會根據圖片的寬高比自動計算出最佳佈局。 + +要創建一個相簿,你只需要將多張圖片放在同一行(或同一個段落)中。 + +### 語法 + +```markdown +![圖片 1](image1.jpg) ![圖片 2](image2.jpg) + +![圖片 3](image3.jpg) ![圖片 4](image4.jpg) +``` + +> **注意**: 圖片之間應該有兩個空格,以確保它們在 Markdown 中保持在同一行。 + +--- + +相簿語法啟發自 [Typlog](https://typlog.com/) diff --git a/themes/stack/demo/content/post/Image Gallery/index.zh.md b/themes/stack/demo/content/post/Image Gallery/index.zh.md new file mode 100644 index 0000000..68ea23d --- /dev/null +++ b/themes/stack/demo/content/post/Image Gallery/index.zh.md @@ -0,0 +1,41 @@ +--- +title: 相册 +description: 展示内置的相册支持 +date: 2026-01-26 +slug: image-gallery +image: helena-hertz-wWZzXlDpMog-unsplash.jpg +categories: + - Documentation +tags: + - Gallery + - Photoswipe +toc: false +--- + +Stack 主题内置了对相册的支持。你只需要简单地将多张图片并排放置,就可以创建一个精美的相册。 + +## 示例相册 + +![Photo by Florian Klauer on Unsplash](florian-klauer-nptLmg6jqDo-unsplash.jpg) ![Photo by Luca Bravo on Unsplash](luca-bravo-alS7ewQ41M8-unsplash.jpg) + +![Photo by Helena Hertz on Unsplash](helena-hertz-wWZzXlDpMog-unsplash.jpg) ![Photo by Hudai Gayiran on Unsplash](hudai-gayiran-3Od_VKcDEAA-unsplash.jpg) + +## 它是如何工作的 + +相册由 **Photoswipe** 和一个自定义的内部脚本驱动。它会根据图片的宽高比自动计算出最佳布局。 + +要创建一个相册,你只需要将多张图片放在同一行(或同一个段落)中。 + +### 语法 + +```markdown +![图片 1](image1.jpg) ![图片 2](image2.jpg) + +![图片 3](image3.jpg) ![图片 4](image4.jpg) +``` + +> **注意**: 图片之间应该有两个空格,以确保它们在 Markdown 中保持在同一行。 + +--- + +相册语法启发自 [Typlog](https://typlog.com/) diff --git a/themes/stack/demo/content/post/Image Gallery/luca-bravo-alS7ewQ41M8-unsplash.jpg b/themes/stack/demo/content/post/Image Gallery/luca-bravo-alS7ewQ41M8-unsplash.jpg new file mode 100644 index 0000000..f6c90bc Binary files /dev/null and b/themes/stack/demo/content/post/Image Gallery/luca-bravo-alS7ewQ41M8-unsplash.jpg differ diff --git a/themes/stack/demo/content/post/Markdown Syntax/index.md b/themes/stack/demo/content/post/Markdown Syntax/index.md new file mode 100644 index 0000000..1b40b14 --- /dev/null +++ b/themes/stack/demo/content/post/Markdown Syntax/index.md @@ -0,0 +1,168 @@ +--- +title: Markdown Syntax Guide +date: 2026-01-25 +description: Sample article showcasing basic Markdown syntax and formatting for HTML elements. +tags: + - markdown + - css + - html + - themes +categories: + - Documentation +image: pawel-czerwinski-8uZPynIu-rQ-unsplash.jpg +--- + +This article offers a sample of basic Markdown syntax that can be used in Hugo content files, also it shows whether basic HTML elements are decorated with CSS in a Hugo theme. + + + +## Headings + +The following HTML `

`—`

` elements represent six levels of section headings. `

` is the highest section level while `

` is the lowest. + +### H3 +#### H4 +##### H5 +###### H6 + +## Paragraph + +Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat. + +Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat. + +## Blockquotes + +The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a `footer` or `cite` element, and optionally with in-line changes such as annotations and abbreviations. + +### Blockquote without attribution + +> Tiam, ad mint andaepu dandae nostion secatur sequo quae. +> **Note** that you can use *Markdown syntax* within a blockquote. + +### Blockquote with attribution + +> Don't communicate by sharing memory, share memory by communicating.
+> — Rob Pike[^1] + +[^1]: The above quote is excerpted from Rob Pike's [talk](https://www.youtube.com/watch?v=PAAkCSZUG1c) during Gopherfest, November 18, 2015. + +### Blockquote with alert + +> [!NOTE] +> Highlights information that users should take into account, even when skimming. + +> [!NOTE] Custom title +> You can also provide a custom title for the note alert. + +> [!TIP] +> Optional information to help a user be more successful. + +> [!IMPORTANT] +> Crucial information necessary for users to succeed. + +> [!WARNING] +> Critical content demanding immediate user attention due to potential risks. + +> [!CAUTION] +> Negative potential consequences of an action. + +## Tables + +Tables aren't part of the core Markdown spec, but Hugo supports supports them out-of-the-box. + + | Name | Age | + | ----- | --- | + | Bob | 27 | + | Alice | 23 | + +### Inline Markdown within tables + +| Italics | Bold | Code | +| --------- | -------- | ------ | +| *italics* | **bold** | `code` | + +| A | B | C | D | E | F | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------- | +| Lorem ipsum dolor sit amet, consectetur adipiscing elit. | Phasellus ultricies, sapien non euismod aliquam, dui ligula tincidunt odio, at accumsan nulla sapien eget ex. | Proin eleifend dictum ipsum, non euismod ipsum pulvinar et. Vivamus sollicitudin, quam in pulvinar aliquam, metus elit pretium purus | Proin sit amet velit nec enim imperdiet vehicula. | Ut bibendum vestibulum quam, eu egestas turpis gravida nec | Sed scelerisque nec turpis vel viverra. Vivamus vitae pretium sapien | + +## Code Blocks +### Code block with backticks + +```html + + + + + Example HTML5 Document + + +

Test

+ + +``` + +### Code block indented with four spaces + + + + + + Example HTML5 Document + + +

Test

+ + + +### Diff code block + +```diff +[dependencies.bevy] +git = "https://github.com/bevyengine/bevy" +rev = "11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13" +- features = ["dynamic"] ++ features = ["jpeg", "dynamic"] +``` + +### One line code block + +```html +

A paragraph

+``` + +## List Types + +### Ordered List + +1. First item +2. Second item +3. Third item + +### Unordered List + +* List item +* Another item +* And another item + +### Nested list + +* Fruit + * Apple + * Orange + * Banana +* Dairy + * Milk + * Cheese + +## Other Elements — abbr, sub, sup, kbd, mark + +GIF is a bitmap image format. + +H2O + +Xn + Yn = Zn + +Press CTRL + ALT + Delete to end the session. + +Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures. \ No newline at end of file diff --git a/themes/stack/demo/content/post/Markdown Syntax/index.zh.md b/themes/stack/demo/content/post/Markdown Syntax/index.zh.md new file mode 100644 index 0000000..14a8c86 --- /dev/null +++ b/themes/stack/demo/content/post/Markdown Syntax/index.zh.md @@ -0,0 +1,168 @@ +--- +title: Markdown 语法指南 +date: 2026-01-25 +description: 展示基本 Markdown 语法和 HTML 元素格式的示例文章。 +tags: + - markdown + - css + - html + - themes +categories: + - Documentation +image: pawel-czerwinski-8uZPynIu-rQ-unsplash.jpg +--- + +这篇文章提供了可以在 Hugo 内容文件中使用的基本 Markdown 语法示例,同时也展示了 Hugo 主题中基本 HTML 元素是否应用了 CSS 装饰。 + + + +## 标题 (Headings) + +以下 HTML `

`—`

` 元素代表了六个级别的章节标题。`

` 是最高级别,而 `

` 是最低级别。 + +### H3 +#### H4 +##### H5 +###### H6 + +## 段落 (Paragraph) + +Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat. + +Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat. + +## 引用 (Blockquotes) + +引用元素代表从另一个来源引用的内容,可以选择带有引用说明(必须在 `footer` 或 `cite` 元素内),也可以选择带有行内更改(如注释和缩写)。 + +### 不带出处的引用 + +> Tiam, ad mint andaepu dandae nostion secatur sequo quae. +> **注意**:你可以在引用中使用 *Markdown 语法*。 + +### 带有出处的引用 + +> 不要通过共享内存来通信,而要通过通信来共享内存。
+> — Rob Pike[^1] + +[^1]: 以上引用摘自 Rob Pike 在 2015 年 11 月 18 日 Gopherfest 期间的[演讲](https://www.youtube.com/watch?v=PAAkCSZUG1c)。 + +### 带提示的引用 + +> [!NOTE] +> 突出显示用户在快速浏览时也应注意的信息。 + +> [!TIP] +> 可选信息,帮助用户更顺利地完成任务。 + +> [!IMPORTANT] +> 用户成功所必需的关键信息。 + +> [!WARNING] +> 由于潜在风险而需要用户立即关注的关键内容。 + +> [!CAUTION] +> 某个操作可能带来的负面后果。 + +> [!NOTE] 自定义标题 +> 如果你想使用自定义标题,可以在方括号后面添加标题文本,如上所示。 + +## 表格 (Tables) + +表格虽然不是 Markdown 核心规范的一部分,但 Hugo 出箱即用地支持它们。 + + | 姓名 | 年龄 | + | ----- | ---- | + | Bob | 27 | + | Alice | 23 | + +### 表格内的行内 Markdown + +| 斜体 | 加粗 | 代码 | +| --------- | -------- | ------ | +| *italics* | **bold** | `code` | + +| A | B | C | D | E | F | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------- | +| Lorem ipsum dolor sit amet, consectetur adipiscing elit. | Phasellus ultricies, sapien non euismod aliquam, dui ligula tincidunt odio, at accumsan nulla sapien eget ex. | Proin eleifend dictum ipsum, non euismod ipsum pulvinar et. Vivamus sollicitudin, quam in pulvinar aliquam, metus elit pretium purus | Proin sit amet velit nec enim imperdiet vehicula. | Ut bibendum vestibulum quam, eu egestas turpis gravida nec | Sed scelerisque nec turpis vel viverra. Vivamus vitae pretium sapien | + +## 代码块 (Code Blocks) +### 使用反引号的代码块 + +```html + + + + + Example HTML5 Document + + +

Test

+ + +``` + +### 使用四个空格缩进的代码块 + + + + + + Example HTML5 Document + + +

Test

+ + + +### Diff 代码块 + +```diff +[dependencies.bevy] +git = "https://github.com/bevyengine/bevy" +rev = "11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13" +- features = ["dynamic"] ++ features = ["jpeg", "dynamic"] +``` + +### 单行代码块 + +```html +

A paragraph

+``` + +## 列表类型 (List Types) + +### 有序列表 + +1. 第一项 +2. 第二项 +3. 第三项 + +### 无序列表 + +* 列表项 +* 另一项 +* 还有一项 + +### 嵌套列表 + +* 水果 + * 苹果 + * 橘子 + * 香蕉 +* 乳制品 + * 牛奶 + * 奶酪 + +## 其他元素 — abbr, sub, sup, kbd, mark + +GIF 是一种位图图像格式。 + +H2O + +Xn + Yn = Zn + +按 CTRL + ALT + Delete 结束会话。 + +大多数蝾螈是夜行性的,捕食昆虫、蠕虫和其他小型生物。 diff --git a/themes/stack/demo/content/post/Markdown Syntax/pawel-czerwinski-8uZPynIu-rQ-unsplash.jpg b/themes/stack/demo/content/post/Markdown Syntax/pawel-czerwinski-8uZPynIu-rQ-unsplash.jpg new file mode 100644 index 0000000..1634e85 Binary files /dev/null and b/themes/stack/demo/content/post/Markdown Syntax/pawel-czerwinski-8uZPynIu-rQ-unsplash.jpg differ diff --git a/themes/stack/demo/content/post/Math Typesetting/index.md b/themes/stack/demo/content/post/Math Typesetting/index.md new file mode 100644 index 0000000..c8ed973 --- /dev/null +++ b/themes/stack/demo/content/post/Math Typesetting/index.md @@ -0,0 +1,61 @@ +--- +title: Math Typesetting +date: 2026-01-24 +description: Guide to enabling and using KaTeX in the Stack theme +math: true +categories: + - Documentation +slug: math-typesetting +--- + +The Stack theme supports rendering mathematical notation using [KaTeX](https://katex.org/). + + + +## Enabling KaTeX + +### Per-page basis + +To enable KaTeX for a specific post, include `math: true` in the post's frontmatter: + +```yaml +--- +title: "My Math Post" +math: true +--- +``` + +### Globally + +To enable KaTeX for all posts, set `article.math` to `true` in your site configuration (`params.yaml` or `params.toml`): + +```yaml +article: + math: true +``` + +## Examples + +### Inline Math + +You can include math inline by wrapping the expression in single dollar signs `$`. + +For example: `$ \varphi = \dfrac{1+\sqrt5}{2}= 1.6180339887… $` renders as $ \varphi = \dfrac{1+\sqrt5}{2}= 1.6180339887… $ + +### Block Math + +For larger equations, use double dollar signs `$$` to create a math block. + +$$ + \varphi = 1+\frac{1} {1+\frac{1} {1+\frac{1} {1+\cdots} } } +$$ + +### More Complex Formula + +$$ +f(a) = \frac{1}{2\pi i} \oint_\gamma \frac{f(z)}{z-a} dz +$$ + +--- + +**Note:** For a full list of supported TeX functions, refer to the [KaTeX documentation](https://katex.org/docs/supported.html). diff --git a/themes/stack/demo/content/post/Math Typesetting/index.zh.md b/themes/stack/demo/content/post/Math Typesetting/index.zh.md new file mode 100644 index 0000000..bf5e9d3 --- /dev/null +++ b/themes/stack/demo/content/post/Math Typesetting/index.zh.md @@ -0,0 +1,61 @@ +--- +title: 数学公式 +date: 2026-01-24 +description: 在 Stack 主题中启用和使用 KaTeX 的指南 +math: true +categories: + - Documentation +slug: math-typesetting +--- + +Stack 主题支持使用 [KaTeX](https://katex.org/) 渲染数学公式。 + + + +## 启用 KaTeX + +### 按页面启用 + +要在特定文章中启用 KaTeX,请在文章的前置参数(frontmatter)中包含 `math: true`: + +```yaml +--- +title: "我的数学文章" +math: true +--- +``` + +### 全局启用 + +要为所有文章启用 KaTeX,请在网站配置(`params.yaml` 或 `params.toml`)中将 `article.math` 设置为 `true`: + +```yaml +article: + math: true +``` + +## 示例 + +### 行内公式 + +你可以通过将表达式包裹在单个美元符号 `$` 中来包含行内公式。 + +例如:`$ \varphi = \dfrac{1+\sqrt5}{2}= 1.6180339887… $` 将渲染为 $ \varphi = \dfrac{1+\sqrt5}{2}= 1.6180339887… $ + +### 块级公式 + +对于较大的方程式,请使用双美元符号 `$$` 创建数学块。 + +$$ + \varphi = 1+\frac{1} {1+\frac{1} {1+\frac{1} {1+\cdots} } } +$$ + +### 更复杂的公式 + +$$ +f(a) = \frac{1}{2\pi i} \oint_\gamma \frac{f(z)}{z-a} dz +$$ + +--- + +**注意:** 有关受支持的 TeX 函数的完整列表,请参阅 [KaTeX 文档](https://katex.org/docs/supported.html)。 diff --git a/themes/stack/demo/content/post/mermaid-diagrams/index.md b/themes/stack/demo/content/post/mermaid-diagrams/index.md new file mode 100644 index 0000000..0914b6f --- /dev/null +++ b/themes/stack/demo/content/post/mermaid-diagrams/index.md @@ -0,0 +1,603 @@ +--- +author: Hugo Authors +title: Mermaid Diagrams +date: 2025-12-23 +description: A comprehensive guide to creating diagrams with Mermaid in Hugo +categories: + - Themes + - Syntax +tags: + - Mermaid + - Diagrams + - Markdown +--- + +This theme supports [Mermaid](https://mermaid.js.org/) diagrams directly in your Markdown content. Mermaid lets you create diagrams and visualizations using text and code. + + + +## About Mermaid.js + +This theme integrates [Mermaid.js](https://mermaid.js.org/) (v11) to render diagrams from text definitions within Markdown code blocks. Mermaid is a JavaScript-based diagramming and charting tool that uses text-based syntax inspired by Markdown. + +For complete syntax documentation, see the [Mermaid.js documentation](https://mermaid.js.org/intro/syntax-reference.html). + +## Getting Started + +To create a Mermaid diagram, simply use a fenced code block with `mermaid` as the language identifier: + +````markdown +```mermaid +graph TD + A[Start] --> B[Process] + B --> C[End] +``` +```` + +The diagram will be automatically rendered when the page loads. + +## Features + +- **Auto-detection**: Mermaid script only loads on pages that contain diagrams +- **Theme Support**: Diagrams automatically adapt to light/dark mode +- **HTML Labels**: Support for HTML content in labels (like `
` for line breaks) +- **Configurable**: Customize version, security level, and more in your site config + +## Configuration + +You can configure Mermaid in your site config: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + version: "11" # Mermaid version from CDN + look: classic # classic or handDrawn (sketch style) + lightTheme: default # Theme for light mode + darkTheme: neutral # Theme for dark mode + securityLevel: strict # strict (default), loose, antiscript, sandbox + htmlLabels: true # Enable HTML in labels +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + version = "11" # Mermaid version from CDN + look = "classic" # classic or handDrawn (sketch style) + lightTheme = "default" # Theme for light mode + darkTheme = "neutral" # Theme for dark mode + securityLevel = "strict" # strict (default), loose, antiscript, sandbox + htmlLabels = true # Enable HTML in labels +``` + +### Additional Global Options + +These optional settings use Mermaid's defaults when not specified: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + maxTextSize: 50000 # Maximum text size (default: 50000) + maxEdges: 500 # Maximum edges allowed (default: 500) + fontSize: 16 # Global font size in pixels (default: 16) + fontFamily: "arial" # Global font family + curve: "basis" # Line curve: basis, cardinal, linear (default: basis) + logLevel: 5 # Debug level 0-5, 0=debug, 5=fatal (default: 5) +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + maxTextSize = 50000 # Maximum text size (default: 50000) + maxEdges = 500 # Maximum edges allowed (default: 500) + fontSize = 16 # Global font size in pixels (default: 16) + fontFamily = "arial" # Global font family + curve = "basis" # Line curve: basis, cardinal, linear (default: basis) + logLevel = 5 # Debug level 0-5, 0=debug, 5=fatal (default: 5) +``` + +For diagram-specific options (like `flowchart.useMaxWidth`), use Mermaid's init directive directly in your diagram: + +````markdown +```mermaid +%%{init: {'flowchart': {'useMaxWidth': false}}}%% +flowchart LR + A --> B +``` +```` + +> **Security Note:** The default `securityLevel: strict` is recommended. Set to `loose` only if you need HTML labels like `
` in your diagrams. + +### Available Themes + +| Theme | Description | +|-------|-------------| +| `default` | Standard colorful theme | +| `neutral` | Grayscale, great for printing and dark mode | +| `dark` | Designed for dark backgrounds | +| `forest` | Green color palette | +| `base` | Minimal theme, customizable with themeVariables | +| `null` | Disable theming entirely | + +### Custom Theme Variables + +For full control, use the `base` theme with custom variables: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + lightTheme: base + darkTheme: base + lightThemeVariables: + primaryColor: "#4a90d9" + primaryTextColor: "#ffffff" + lineColor: "#333333" + darkThemeVariables: + primaryColor: "#6ab0f3" + primaryTextColor: "#ffffff" + lineColor: "#cccccc" + background: "#1a1a2e" +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + lightTheme = "base" + darkTheme = "base" + + [params.article.mermaid.lightThemeVariables] + primaryColor = "#4a90d9" + primaryTextColor = "#ffffff" + lineColor = "#333333" + + [params.article.mermaid.darkThemeVariables] + primaryColor = "#6ab0f3" + primaryTextColor = "#ffffff" + lineColor = "#cccccc" + background = "#1a1a2e" +``` + +Common variables: `primaryColor`, `secondaryColor`, `tertiaryColor`, `primaryTextColor`, `lineColor`, `background`, `fontFamily` + +> **Note:** Theme variables only work with the `base` theme and must use hex color values (e.g., `#ff0000`). + +## Diagram Types + +### Flowchart + +Flowcharts are the most common diagram type. Use `graph` or `flowchart` with direction indicators: +- `TD` or `TB`: Top to bottom +- `BT`: Bottom to top +- `LR`: Left to right +- `RL`: Right to left + +```mermaid +flowchart LR + A[Hard edge] -->|Link text| B(Round edge) + B --> C{Decision} + C -->|One| D[Result one] + C -->|Two| E[Result two] +``` + +### Sequence Diagram + +Perfect for showing interactions between components: + +```mermaid +sequenceDiagram + participant Alice + participant Bob + Alice->>John: Hello John, how are you? + loop Healthcheck + John->>John: Fight against hypochondria + end + Note right of John: Rational thoughts
prevail! + John-->>Alice: Great! + John->>Bob: How about you? + Bob-->>John: Jolly good! +``` + +### Class Diagram + +Visualize class structures and relationships: + +```mermaid +classDiagram + Animal <|-- Duck + Animal <|-- Fish + Animal <|-- Zebra + Animal : +int age + Animal : +String gender + Animal: +isMammal() + Animal: +mate() + class Duck{ + +String beakColor + +swim() + +quack() + } + class Fish{ + -int sizeInFeet + -canEat() + } + class Zebra{ + +bool is_wild + +run() + } +``` + +### State Diagram + +Model state machines and transitions: + +```mermaid +stateDiagram-v2 + [*] --> Still + Still --> [*] + + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*] +``` + +### Entity Relationship Diagram + +Document database schemas: + +```mermaid +erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + CUSTOMER }|..|{ DELIVERY-ADDRESS : uses + CUSTOMER { + string name + string custNumber + string sector + } + ORDER { + int orderNumber + string deliveryAddress + } +``` + +### Gantt Chart + +Plan and track project schedules: + +```mermaid +gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + section Section + A task :a1, 2024-01-01, 30d + Another task :after a1, 20d + section Another + Task in Another :2024-01-12, 12d + another task :24d +``` + +### Pie Chart + +Display proportional data: + +```mermaid +pie showData + title Key elements in Product X + "Calcium" : 42.96 + "Potassium" : 50.05 + "Magnesium" : 10.01 + "Iron" : 5 +``` + +### Git Graph + +Visualize Git branching strategies: + +```mermaid +gitGraph + commit + commit + branch develop + checkout develop + commit + commit + checkout main + merge develop + commit + commit +``` + +### Mindmap + +Create hierarchical mindmaps: + +```mermaid +mindmap + root((mindmap)) + Origins + Long history + Popularisation + British popular psychology author Tony Buzan + Research + On effectiveness
and features + On Automatic creation + Uses + Creative techniques + Strategic planning + Argument mapping + Tools + Pen and paper + Mermaid +``` + +### Timeline + +Display chronological events: + +```mermaid +timeline + title History of Social Media Platform + 2002 : LinkedIn + 2004 : Facebook + : Google + 2005 : YouTube + 2006 : Twitter +``` + +## Advanced Features + +### HTML in Labels + +To use HTML in labels, you must set `securityLevel: loose` in your site config: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + securityLevel: loose + htmlLabels: true +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + securityLevel = "loose" + htmlLabels = true +``` + +Then you can use HTML tags like `
` for line breaks: + +````markdown +```mermaid +graph TD + A[Line 1
Line 2] --> B[Bold text] +``` +```` + +### Per-Diagram Theming + +Override the theme for a specific diagram using Mermaid's frontmatter: + +````markdown +```mermaid +%%{init: {'theme': 'forest'}}%% +graph TD + A[Start] --> B[End] +``` +```` + +```mermaid +%%{init: {'theme': 'forest'}}%% +graph TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[Car] +``` + +### Inline Styling with `style` + +You can style individual nodes directly within your diagram using the `style` directive: + +````markdown +```mermaid +flowchart LR + A[Start] --> B[Process] --> C[End] + style A fill:#4ade80,stroke:#166534,color:#000 + style B fill:#60a5fa,stroke:#1e40af,color:#000 + style C fill:#f87171,stroke:#991b1b,color:#fff +``` +```` + +**Result:** + +```mermaid +flowchart LR + A[Start] --> B[Process] --> C[End] + style A fill:#4ade80,stroke:#166534,color:#000 + style B fill:#60a5fa,stroke:#1e40af,color:#000 + style C fill:#f87171,stroke:#991b1b,color:#fff +``` + +Style properties include: +- `fill` - Background color +- `stroke` - Border color +- `stroke-width` - Border thickness +- `color` - Text color +- `stroke-dasharray` - Dashed border (e.g., `5 5`) + +### Styling with CSS Classes + +You can define reusable styles with `classDef` and apply them using `:::className`: + +````markdown +```mermaid +flowchart LR + A:::success --> B:::info --> C:::warning + classDef success fill:#4ade80,stroke:#166534,color:#000 + classDef info fill:#60a5fa,stroke:#1e40af,color:#000 + classDef warning fill:#fbbf24,stroke:#92400e,color:#000 +``` +```` + +**Result:** + +```mermaid +flowchart LR + A:::success --> B:::info --> C:::warning + classDef success fill:#4ade80,stroke:#166534,color:#000 + classDef info fill:#60a5fa,stroke:#1e40af,color:#000 + classDef warning fill:#fbbf24,stroke:#92400e,color:#000 +``` + +### Subgraphs + +Group related nodes together: + +```mermaid +flowchart TB + subgraph one + a1-->a2 + end + subgraph two + b1-->b2 + end + subgraph three + c1-->c2 + end + one --> two + three --> two + two --> c2 +``` + +## Theme Switching + +This theme automatically detects your site's light/dark mode preference and adjusts the Mermaid diagram theme accordingly: + +- **Light mode**: Uses the `default` Mermaid theme +- **Dark mode**: Uses the `dark` Mermaid theme (configurable) + +Try toggling the theme switcher to see diagrams update in real-time! + +## Complex Example + +Here's an example with subgraphs, HTML labels, emojis, and custom styling: + +```mermaid +flowchart TD + subgraph client["👤 Client"] + A["User Device
192.168.1.10"] + end + + subgraph cloud["☁️ Cloud Gateway"] + B["Load Balancer
(SSL Termination)"] + end + + subgraph server["🖥️ Application Server"] + C["API Gateway
10.0.0.1"] + D["Auth Service
10.0.0.2"] + E["Web Server
10.0.0.3"] + F["Database
10.0.0.4"] + end + + A -- "HTTPS Request" --> B + B -- "Forward
(internal)" --> C + C -- "Authenticate" --> D + D -- "Token" --> C + C -- "Route" --> E + E --> F + + style client fill:#1a365d,stroke:#2c5282,color:#fff + style cloud fill:#f6ad55,stroke:#dd6b20,color:#000 + style server fill:#276749,stroke:#22543d,color:#fff +``` + +> **Note:** This example requires `securityLevel: loose` for HTML labels and styling to work. + +## Known Limitations + +### Dark Mode Theming + +Mermaid.js's built-in themes have some limitations: + +- **`dark` theme** (default): Best text contrast, but some diagram backgrounds may appear brownish (e.g., Gantt charts) +- **`neutral` theme**: Better background colors, but some text (labels, legends) may have reduced contrast + +**For full control**, use the `base` theme with custom variables: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + darkTheme: base + darkThemeVariables: + primaryColor: "#1f2937" + primaryTextColor: "#ffffff" + lineColor: "#9ca3af" + textColor: "#e5e7eb" +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + darkTheme = "base" + + [params.article.mermaid.darkThemeVariables] + primaryColor = "#1f2937" + primaryTextColor = "#ffffff" + lineColor = "#9ca3af" + textColor = "#e5e7eb" +``` + +We plan to improve dark mode theming in future updates as Mermaid.js evolves. + +## Troubleshooting + +### Diagram not rendering? + +1. Make sure you're using a fenced code block with `mermaid` as the language +2. Check your browser's console for syntax errors +3. Verify your Mermaid syntax at [Mermaid Live Editor](https://mermaid.live/) + +### HTML not working in labels? + +HTML in labels requires `securityLevel: loose`. Update your configuration: + +**hugo.yaml:** +```yaml +params: + article: + mermaid: + securityLevel: loose + htmlLabels: true +``` + +**hugo.toml:** +```toml +[params.article.mermaid] + securityLevel = "loose" + htmlLabels = true +``` + +> **Warning:** Using `loose` security level allows HTML in diagrams. Only use this if you trust your diagram content. + +### Syntax errors? + +Mermaid is strict about syntax. Common issues: +- Missing spaces around arrows +- Unclosed brackets or quotes +- Invalid node IDs (avoid special characters) + +## Resources + +- [Mermaid Documentation](https://mermaid.js.org/intro/) +- [Mermaid Live Editor](https://mermaid.live/) - Test diagrams interactively +- [Mermaid Syntax Reference](https://mermaid.js.org/intro/syntax-reference.html) diff --git a/themes/stack/demo/content/post/pagination-test-01.md b/themes/stack/demo/content/post/pagination-test-01.md new file mode 100644 index 0000000..8f070d4 --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-01.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 01 +date: 2000-01-01 +description: Test post 01 for pagination behavior. +slug: pagination-test-01 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **01**. diff --git a/themes/stack/demo/content/post/pagination-test-02.md b/themes/stack/demo/content/post/pagination-test-02.md new file mode 100644 index 0000000..97ecbd6 --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-02.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 02 +date: 2000-01-02 +description: Test post 02 for pagination behavior. +slug: pagination-test-02 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **02**. diff --git a/themes/stack/demo/content/post/pagination-test-03.md b/themes/stack/demo/content/post/pagination-test-03.md new file mode 100644 index 0000000..365ca06 --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-03.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 03 +date: 2000-01-03 +description: Test post 03 for pagination behavior. +slug: pagination-test-03 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **03**. diff --git a/themes/stack/demo/content/post/pagination-test-04.md b/themes/stack/demo/content/post/pagination-test-04.md new file mode 100644 index 0000000..6e109b8 --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-04.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 04 +date: 2000-01-04 +description: Test post 04 for pagination behavior. +slug: pagination-test-04 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **04**. diff --git a/themes/stack/demo/content/post/pagination-test-05.md b/themes/stack/demo/content/post/pagination-test-05.md new file mode 100644 index 0000000..8f1ef6e --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-05.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 05 +date: 2000-01-05 +description: Test post 05 for pagination behavior. +slug: pagination-test-05 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **05**. diff --git a/themes/stack/demo/content/post/pagination-test-06.md b/themes/stack/demo/content/post/pagination-test-06.md new file mode 100644 index 0000000..63c94c1 --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-06.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 06 +date: 2000-01-06 +description: Test post 06 for pagination behavior. +slug: pagination-test-06 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **06**. diff --git a/themes/stack/demo/content/post/pagination-test-07.md b/themes/stack/demo/content/post/pagination-test-07.md new file mode 100644 index 0000000..b7ab7d2 --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-07.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 07 +date: 2000-01-07 +description: Test post 07 for pagination behavior. +slug: pagination-test-07 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **07**. diff --git a/themes/stack/demo/content/post/pagination-test-08.md b/themes/stack/demo/content/post/pagination-test-08.md new file mode 100644 index 0000000..e9eb51a --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-08.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 08 +date: 2000-01-08 +description: Test post 08 for pagination behavior. +slug: pagination-test-08 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **08**. diff --git a/themes/stack/demo/content/post/pagination-test-09.md b/themes/stack/demo/content/post/pagination-test-09.md new file mode 100644 index 0000000..b8a49a2 --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-09.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 09 +date: 2000-01-09 +description: Test post 09 for pagination behavior. +slug: pagination-test-09 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **09**. diff --git a/themes/stack/demo/content/post/pagination-test-10.md b/themes/stack/demo/content/post/pagination-test-10.md new file mode 100644 index 0000000..fc97c0c --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-10.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 10 +date: 2000-01-10 +description: Test post 10 for pagination behavior. +slug: pagination-test-10 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **10**. diff --git a/themes/stack/demo/content/post/pagination-test-11.md b/themes/stack/demo/content/post/pagination-test-11.md new file mode 100644 index 0000000..a428121 --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-11.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 11 +date: 2000-01-11 +description: Test post 11 for pagination behavior. +slug: pagination-test-11 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **11**. diff --git a/themes/stack/demo/content/post/pagination-test-12.md b/themes/stack/demo/content/post/pagination-test-12.md new file mode 100644 index 0000000..3273c0a --- /dev/null +++ b/themes/stack/demo/content/post/pagination-test-12.md @@ -0,0 +1,14 @@ +--- +title: Pagination Test 12 +date: 2000-01-12 +description: Test post 12 for pagination behavior. +slug: pagination-test-12 +tags: + - pagination + - test +categories: + - Testing +draft: false +--- + +This is pagination test post **12**. diff --git a/themes/stack/demo/content/post/shortcodes/index.ja.md b/themes/stack/demo/content/post/shortcodes/index.ja.md new file mode 100644 index 0000000..7ff93cb --- /dev/null +++ b/themes/stack/demo/content/post/shortcodes/index.ja.md @@ -0,0 +1,119 @@ +--- +title: ショートコード +date: 2026-01-26 +description: Stack テーマでサポートされているさまざまなショートコードを紹介します。 +categories: + - Documentation +tags: + - 隐私 +--- + +Stack テーマには、コンテンツを強化するためのカスタムショートコードがいくつか用意されています。 + + + +## 引用 (Quote) + +`quote` ショートコードを使用すると、著者、出典、URL を含む引用を表示できます。 + +{{< quote author="有名な人" source="彼らが書いた本" url="https://ja.wikipedia.org/wiki/Book">}} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +{{< /quote >}} + +### 使い方 + +```markdown +{{}} +ここに引用内容を入力します。 +{{}} +``` + + +## ビデオ (Video) + +`video` ショートコードを使用すると、セルフホストまたはリモートのビデオファイルを埋め込むことができます。 + +{{< video src="https://www.w3schools.com/html/mov_bbb.mp4" >}} + +### 使い方 + +```markdown +{{}} +``` + + +## Bilibili + +Bilibili からビデオを埋め込みます。`av` および `bv` ID の両方をサポートしています。 + +{{< bilibili "BV1634y1t7xR" >}} + +### 使い方 + +```markdown +{{}} +``` + + +## YouTube + +Hugo 内蔵の YouTube ショートコードです。 + +{{< youtube ZJthWmvUzzc >}} + +### 使い方 + +```markdown +{{}} +``` + + +## 騰訊視頻 (Tencent Video) + +Tencent Video からビデオを埋め込みます。 + +{{< tencent "u00306ng962" >}} + +### 使い方 + +```markdown +{{}} +``` + + +## GitLab Snippet + +GitLab からスニペットを埋め込みます。 + +{{< gitlab 2349278 >}} + +### 使い方 + +```markdown +{{}} +``` + + +## 図表 (Diagrams) + +Stack は [Mermaid](https://mermaid.js.org/) 図表を標準でサポートしています。 + +```mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +``` + +### 使い方 + +Mermaid コードを、言語を `mermaid` に設定したコードブロックで囲みます。 + +
```mermaid
+graph TD;
+    A-->B;
+    A-->C;
+    B-->D;
+    C-->D;
+```
diff --git a/themes/stack/demo/content/post/shortcodes/index.md b/themes/stack/demo/content/post/shortcodes/index.md new file mode 100644 index 0000000..7d8ec3a --- /dev/null +++ b/themes/stack/demo/content/post/shortcodes/index.md @@ -0,0 +1,116 @@ +--- +title: Shortcodes +date: 2026-01-26 +description: Sample article showcasing various shortcodes supported by Stack theme. +categories: + - Documentation +tags: + - shortcodes + - privacy +--- + +Stack theme also provides some custom shortcodes to enhance your content. + + + +## Quote + +The `quote` shortcode allows you to display a quote with an author, source, and URL. + +{{< quote author="A famous person" source="The book they wrote" url="https://en.wikipedia.org/wiki/Book">}} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +{{< /quote >}} + +### Usage + +```markdown +{{}} +Quote content here. +{{}} +``` + +## Video + +The `video` shortcode allows you to embed self-hosted or remote video files. + +{{< video src="https://www.w3schools.com/html/mov_bbb.mp4" >}} + +### Usage + +```markdown +{{}} +``` + +## Bilibili + +Embed videos from Bilibili. Supports both `av` and `bv` IDs. + +{{< bilibili "BV1634y1t7xR" >}} + +### Usage + +```markdown +{{}} +``` + +## YouTube + +Hugo's built-in YouTube shortcode. + +{{< youtube ZJthWmvUzzc >}} + +### Usage + +```markdown +{{}} +``` + +## Tencent Video + +Embed videos from Tencent Video. + +{{< tencent "u00306ng962" >}} + +### Usage + +```markdown +{{}} +``` + +## GitLab Snippet + +Embed snippets from GitLab. + +{{< gitlab 2349278 >}} + +### Usage + +```markdown +{{}} +``` + +## Diagrams + +Stack supports [Mermaid](https://mermaid.js.org/) diagrams out of the box. + +```mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +``` + +### Usage + +Wrap your Mermaid code in a code block with the language set to `mermaid`. + +
```mermaid
+graph TD;
+    A-->B;
+    A-->C;
+    B-->D;
+    C-->D;
+```
+ + diff --git a/themes/stack/demo/content/post/shortcodes/index.zh-hant-tw.md b/themes/stack/demo/content/post/shortcodes/index.zh-hant-tw.md new file mode 100644 index 0000000..d9b7c00 --- /dev/null +++ b/themes/stack/demo/content/post/shortcodes/index.zh-hant-tw.md @@ -0,0 +1,116 @@ +--- +title: 短代碼 +date: 2026-01-26 +description: 展示 Stack 主題支持的各種短代碼。 +categories: + - Documentation +tags: + - 隐私 +--- + +Stack 主題還提供了一些自定義短代碼來增強你的內容。 + + + +## 引用 (Quote) + +`quote` 短代碼允許你顯示帶有作者、來源和 URL 的引用。 + +{{< quote author="一位名人" source="他們寫的書" url="https://zh.wikipedia.org/wiki/Book">}} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +{{< /quote >}} + +### 用法 + +```markdown +{{}} +這裡是引用內容。 +{{}} +``` + +## 視頻 (Video) + +`video` 短代碼允許你嵌入自託管或遠程視頻文件。 + +{{< video src="https://www.w3schools.com/html/mov_bbb.mp4" >}} + +### 用法 + +```markdown +{{}} +``` + +## Bilibili + +嵌入來自 Bilibili 的視頻。支持 `av` 和 `bv` 號。 + +{{< bilibili "BV1634y1t7xR" >}} + +### 用法 + +```markdown +{{}} +``` + +## YouTube + +Hugo 內置的 YouTube 短代碼。 + +{{< youtube ZJthWmvUzzc >}} + +### 用法 + +```markdown +{{}} +``` + + +## 騰訊視頻 (Tencent Video) + +嵌入來自騰訊視頻的視頻。 + +{{< tencent "u00306ng962" >}} + +### 用法 + +```markdown +{{}} +``` + + +## GitLab Snippet + +嵌入來自 GitLab 的代碼片段。 + +{{< gitlab 2349278 >}} + +### 用法 + +```markdown +{{}} +``` + + +## 圖表 (Diagrams) + +Stack 出箱即用地支持 [Mermaid](https://mermaid.js.org/) 圖表。 + +```mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +``` + +### 用法 + +將你的 Mermaid 代碼包裹在語言設置為 `mermaid` 的代碼塊中。 + +
```mermaid
+graph TD;
+    A-->B;
+    A-->C;
+    B-->D;
+    C-->D;
+```
diff --git a/themes/stack/demo/content/post/shortcodes/index.zh.md b/themes/stack/demo/content/post/shortcodes/index.zh.md new file mode 100644 index 0000000..8bade26 --- /dev/null +++ b/themes/stack/demo/content/post/shortcodes/index.zh.md @@ -0,0 +1,113 @@ +--- +title: 短代码 +date: 2026-01-26 +description: 展示 Stack 主题支持的各种短代码。 +categories: + - Documentation +tags: + - 隐私 +--- + +Stack 主题还提供了一些自定义短代码来增强你的内容。 + + + +## 引用 (Quote) + +`quote` 短代码允许你显示带有作者、来源和 URL 的引用。 + +{{< quote author="一位名人" source="他们写的书" url="https://zh.wikipedia.org/wiki/Book">}} +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +{{< /quote >}} + +### 用法 + +```markdown +{{}} +这里是引用内容。 +{{}} +``` + +## 视频 (Video) + +`video` 短代码允许你嵌入自托管或远程视频文件。 + +{{< video src="https://www.w3schools.com/html/mov_bbb.mp4" >}} + +### 用法 + +```markdown +{{}} +``` + +## Bilibili + +嵌入来自 Bilibili 的视频。支持 `av` 和 `bv` 号。 + +{{< bilibili "BV1634y1t7xR" >}} + +### 用法 + +```markdown +{{}} +``` + +## YouTube + +Hugo 内置的 YouTube 短代码。 + +{{< youtube ZJthWmvUzzc >}} + +### 用法 + +```markdown +{{}} +``` + +## 腾讯视频 (Tencent Video) + +嵌入来自腾讯视频的视频。 + +{{< tencent "u00306ng962" >}} + +### 用法 + +```markdown +{{}} +``` + +## GitLab Snippet + +嵌入来自 GitLab 的代码片段。 + +{{< gitlab 2349278 >}} + +### 用法 + +```markdown +{{}} +``` + +## 图表 (Diagrams) + +Stack 出箱即用地支持 [Mermaid](https://mermaid.js.org/) 图表。 + +```mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +``` + +### 用法 + +将你的 Mermaid 代码包裹在语言设置为 `mermaid` 的代码块中。 + +
```mermaid
+graph TD;
+    A-->B;
+    A-->C;
+    B-->D;
+    C-->D;
+```
diff --git a/themes/stack/demo/go.mod b/themes/stack/demo/go.mod new file mode 100644 index 0000000..f918063 --- /dev/null +++ b/themes/stack/demo/go.mod @@ -0,0 +1,7 @@ +module github.com/CaiJimmy/hugo-theme-stack/demo + +go 1.25.6 + +replace github.com/CaiJimmy/hugo-theme-stack/v3 => ../ + +require github.com/CaiJimmy/hugo-theme-stack/v3 v3.34.0 // indirect diff --git a/themes/stack/demo/run.sh b/themes/stack/demo/run.sh new file mode 100755 index 0000000..98f6d99 --- /dev/null +++ b/themes/stack/demo/run.sh @@ -0,0 +1 @@ +hugo server --gc --themesDir=../.. \ No newline at end of file diff --git a/themes/stack/go.mod b/themes/stack/go.mod new file mode 100644 index 0000000..e75e573 --- /dev/null +++ b/themes/stack/go.mod @@ -0,0 +1,3 @@ +module github.com/CaiJimmy/hugo-theme-stack/v4 + +go 1.17 diff --git a/themes/stack/i18n/ar.toml b/themes/stack/i18n/ar.toml new file mode 100644 index 0000000..155eac0 --- /dev/null +++ b/themes/stack/i18n/ar.toml @@ -0,0 +1,82 @@ +toggleMenu = "اخفي القائمة" +darkMode = "الوضع الداكن" + +[list] + section = "قسم" + + [list.page] + one = "{{ .Count }} صفحه" + other = "{{ .Count }} صفحات" + + [list.subsection] + one = "قسم فرعي" + other = "اقسام فرعية" + +[article] + back = "خلف" + tableOfContents = "جدول المحتويات" + relatedContent = "محتوى مشابهه" + lastUpdatedOn = "التعديل الاخير" + + [article.readingTime] + one = "تُقرأ خلال دقيقة" + other = "تُقرأ خلال {{ .Count }} دقائق" + + [article.alert] + note = "ملاحظة" + tip = "نصيحة" + important = "مهم" + warning = "تحذير" + caution = "تنبيه" + + [article.codeblock] + copy = "نسخ" + copied = "تم النسخ!" + +[notFound] + title = "غير موجود" + subtitle = "تعذر العثور على الصفحة المطلوبة." + +[widget] + [widget.archives] + title = "الارشيفات" + more = "اكثر" + + [widget.tagCloud] + title = "وسوم" + + [widget.categoriesCloud] + title = "التصنيفات" + +[search] + title = "بحث" + placeholder = "اكتب..." + resultTitle = "#PAGES_COUNT نتيجة (#TIME_SECONDS ثواني)" + +[footer] + builtWith = "مبني باستخدام {{ .Generator }}" + designedBy = "قالب {{ .Theme }} مصمم من {{ .DesignedBy }}" + +[cookies] + title = "موافقة ملفات تعريف الارتباط" + text = "نستخدم ملفات تعريف الارتباط لتحسين تجربة التصفح وتحليل حركة المرور على الموقع." + acceptAll = "قبول الكل" + deny = "رفض" + managePreferences = "إدارة التفضيلات" + settingsTitle = "تفضيلات ملفات تعريف الارتباط" + savePreferences = "حفظ التفضيلات" + cancel = "إلغاء" + footerLink = "إعدادات ملفات تعريف الارتباط" + commentsDisabled = "التعليقات معطلة حتى تقبل ملفات تعريف الارتباط الوظيفية." + + [cookies.necessary] + title = "ضروري" + text = "ملفات تعريف الارتباط هذه مطلوبة لعمل الموقع ولا يمكن تعطيلها." + + [cookies.analytics] + title = "التحليلات" + text = "تساعدنا ملفات تعريف الارتباط هذه على فهم كيفية تفاعل الزوار مع موقعنا." + + [cookies.functional] + title = "وظيفي" + text = "تتيح ملفات تعريف الارتباط هذه ميزات مثل التعليقات والمحتوى المضمن." diff --git a/themes/stack/i18n/be.toml b/themes/stack/i18n/be.toml new file mode 100644 index 0000000..e4f4552 --- /dev/null +++ b/themes/stack/i18n/be.toml @@ -0,0 +1,80 @@ +toggleMenu = "Паказаць/схаваць меню" +darkMode = "Цёмны рэжым" + +[list] + section = "Раздзел" + + [list.page] + one = "{{ .Count }} старонка" + few = "{{ .Count }} старонкі" + many = "{{ .Count }} старонак" + other = "{{ .Count }} старонкі" + + [list.subsection] + one = "Падраздзел" + few = "Падраздзелы" + many = "Падраздзелы" + other = "Падраздзелы" + +[article] + back = "Назад" + relatedContent = "Таксама рэкамендуем" + lastUpdatedOn = "Абноўлена" + tableOfContents = "Змест" + readingTime = "Час чытання: {{ .Count }} хв." + + [article.alert] + note = "Заўвага" + tip = "Парада" + important = "Важна" + warning = "Папярэджанне" + caution = "Асцярога" + + [article.codeblock] + copy = "Капіяваць" + copied = "Скапіявана!" + +[notFound] + title = "Не знойдзена" + subtitle = "Запытваемая старонка не існуе" + +[widget] + [widget.archives] + title = "Архівы" + more = "Яшчэ" + + [widget.tagCloud] + title = "Тэгі" + +[search] + title = "Пошук" + placeholder = "Увядзіце нешта..." + resultTitle = "Знайдзена #PAGES_COUNT старонак (за #TIME_SECONDS с.)" + +[footer] + builtWith = "Створана пры дапамозе {{ .Generator }}" + designedBy = "Тэма {{ .Theme }}, дызайн {{ .DesignedBy }}" + +[cookies] + title = "Згода на файлы cookie" + text = "Мы выкарыстоўваем файлы cookie для паляпшэння вашага досведу і аналізу трафіку сайта." + acceptAll = "Прыняць усё" + deny = "Адхіліць" + managePreferences = "Кіраваць налады" + settingsTitle = "Налады файлаў cookie" + savePreferences = "Захаваць налады" + cancel = "Скасаваць" + footerLink = "Налады файлаў cookie" + commentsDisabled = "Каментары адключаны, пакуль вы не прымеце функцыянальныя файлы cookie." + + [cookies.necessary] + title = "Неабходныя" + text = "Гэтыя файлы cookie неабходныя для працы сайта і не могуць быць адключаны." + + [cookies.analytics] + title = "Аналітыка" + text = "Гэтыя файлы cookie дапамагаюць нам зразумець, як наведвальнікі ўзаемадзейнічаюць з нашым сайтам." + + [cookies.functional] + title = "Функцыянальныя" + text = "Гэтыя файлы cookie ўключаюць такія функцыі, як каментары і ўбудаваны кантэнт." diff --git a/themes/stack/i18n/bg.toml b/themes/stack/i18n/bg.toml new file mode 100644 index 0000000..0a4cebf --- /dev/null +++ b/themes/stack/i18n/bg.toml @@ -0,0 +1,82 @@ +toggleMenu = "Покажи Меню" +darkMode = "Тъмен Режим" + +[list] + section = "Секция" + + [list.page] + one = "{{ .Count }} страница" + other = "{{ .Count }} страници" + + [list.subsection] + one = "Подсекция" + other = "Подсекции" + +[article] + back = "Назад" + tableOfContents = "Съдържание" + relatedContent = "Свързано Съдържание" + lastUpdatedOn = "Последна промяна на" + + [article.readingTime] + one = "{{ .Count }} minute read" + other = "{{ .Count }} minute read" + + [article.alert] + note = "Бележка" + tip = "Съвет" + important = "Важно" + warning = "Предупреждение" + caution = "Внимание" + + [article.codeblock] + copy = "Копирай" + copied = "Копирано!" + +[notFound] + title = "Не е намерено" + subtitle = "Страницата която търсите не е открита" + +[widget] + [widget.archives] + title = "Архиви" + more = "Повече" + + [widget.tagCloud] + title = "Тагове" + + [widget.categoriesCloud] + title = "Категории" + +[search] + title = "Търсене" + placeholder = "Напишете нещо..." + resultTitle = "#PAGES_COUNT страници (#TIME_SECONDS секунди)" + +[footer] + builtWith = "Създадено с {{ .Generator }}" + designedBy = "Тема {{ .Theme }} създадена от {{ .DesignedBy }}" + +[cookies] + title = "Съгласие за бисквитки" + text = "Използваме бисквитки, за да подобрим вашето изживяване и да анализираме трафика на сайта." + acceptAll = "Приемане на всички" + deny = "Откажи" + managePreferences = "Управление на предпочитанията" + settingsTitle = "Предпочитания за бисквитки" + savePreferences = "Запазване на предпочитанията" + cancel = "Отказ" + footerLink = "Настройки на бисквитките" + commentsDisabled = "Коментарите са деактивирани, докато не приемете функционални бисквитки." + + [cookies.necessary] + title = "Необходими" + text = "Тези бисквитки са необходими за функционирането на уебсайта и не могат да бъдат деактивирани." + + [cookies.analytics] + title = "Анализи" + text = "Тези бисквитки ни помагат да разберем как посетителите взаимодействат с нашия уебсайт." + + [cookies.functional] + title = "Функционални" + text = "Тези бисквитки активират функции като коментари и вградено съдържание." diff --git a/themes/stack/i18n/bn.toml b/themes/stack/i18n/bn.toml new file mode 100644 index 0000000..d161a06 --- /dev/null +++ b/themes/stack/i18n/bn.toml @@ -0,0 +1,82 @@ +toggleMenu = "টগল মেনু" +darkMode = "ডার্ক মোড" + +[list] + section = "অনুচ্ছেদ" + + [list.page] + one = "{{ .Count }} পাতা" + other = "{{ .Count }} পাতা" + + [list.subsection] + one = "উপ-অনুচ্ছেদ" + other = "উপ-অনুচ্ছেদ" + +[article] + back = "পেছনে" + tableOfContents = "সূচিপত্র" + relatedContent = "সম্পর্কিত বিষয়বস্তু" + lastUpdatedOn = "সর্বশেষ আপডেট করা হয়েছে" + + [article.readingTime] + one = "{{ .Count }} মিনিটে পড়া যাবে" + other = "{{ .Count }} মিনিটে পড়া যাবে" + + [article.alert] + note = "টীকা" + tip = "পরামর্শ" + important = "গুরুত্বপূর্ণ" + warning = "সতর্কতা" + caution = "সাবধানতা" + + [article.codeblock] + copy = "কপি" + copied = "কপি হয়েছে!" + +[notFound] + title = "পাওয়া যায়নি" + subtitle = "এই পাতাটি বিদ্যমান নেই" + +[widget] + [widget.archives] + title = "আর্কাইভ" + more = "আরও" + + [widget.tagCloud] + title = "ট্যাগ" + + [widget.categoriesCloud] + title = "বিভাগ" + +[search] + title = "অনুসন্ধান" + placeholder = "কিছু টাইপ করুন..." + resultTitle = "#PAGES_COUNT পাতা (#TIME_SECONDS সেকেন্ড)" + +[footer] + builtWith = "{{ .Generator }} দিয়ে নির্মিত" + designedBy = "থিম {{ .Theme }} ডিজাইন করেছেন {{ .DesignedBy }}" + +[cookies] + title = "কুকি সম্মতি" + text = "আমরা আপনার ব্রাউজিং অভিজ্ঞতা উন্নত করতে এবং সাইট ট্রাফিক বিশ্লেষণ করতে কুকি ব্যবহার করি।" + acceptAll = "সবগুলো গ্রহণ করুন" + deny = "প্রত্যাখ্যান" + managePreferences = "পছন্দসমূহ পরিচালনা করুন" + settingsTitle = "কুকি পছন্দসমূহ" + savePreferences = "পছন্দসমূহ সংরক্ষণ করুন" + cancel = "বাতিল" + footerLink = "কুকি সেটিংস" + commentsDisabled = "আপনি কার্যকরী কুকি গ্রহণ না করা পর্যন্ত মন্তব্য অক্ষম করা আছে।" + + [cookies.necessary] + title = "প্রয়োজনীয়" + text = "এই কুকিগুলি ওয়েবসাইট কাজ করার জন্য প্রয়োজনীয় এবং নিষ্ক্রিয় করা যায় না।" + + [cookies.analytics] + title = "বিশ্লেষণ" + text = "এই কুকিগুলি আমাদের বুঝতে সাহায্য করে যে দর্শকরা কীভাবে আমাদের ওয়েবসাইটের সাথে ইন্টারঅ্যাক্ট করে।" + + [cookies.functional] + title = "কার্যকরী" + text = "এই কুকিগুলি মন্তব্য এবং এম্বেড করা বিষয়বস্তুর মতো বৈশিষ্ট্যগুলি সক্ষম করে." diff --git a/themes/stack/i18n/ca.toml b/themes/stack/i18n/ca.toml new file mode 100644 index 0000000..82f4d6d --- /dev/null +++ b/themes/stack/i18n/ca.toml @@ -0,0 +1,82 @@ +toggleMenu = "Toggle Menu" +darkMode = "Mode fosc" + +[list] + section = "Secció" + + [list.page] + one = "{{ .Count }} pàgina" + other = "{{ .Count }} pàgines" + + [list.subsection] + one = "Subsecció" + other = "Subseccions" + +[article] + back = "Tornar" + tableOfContents = "Taula de contingut" + relatedContent = "Continguts relacionats" + lastUpdatedOn = "Última vegada actualitzat" + + [article.readingTime] + one = "{{ .Count }} minut a llegir" + other = "{{ .Count }} minuts a llegir" + + [article.alert] + note = "Nota" + tip = "Consell" + important = "Important" + warning = "Avís" + caution = "Precaució" + + [article.codeblock] + copy = "Copia" + copied = "Copiat!" + +[notFound] + title = "No Trobat" + subtitle = "Aquesta pàgina no existeix" + +[widget] + [widget.archives] + title = "Arxiu" + more = "Més" + + [widget.tagCloud] + title = "Etiquetes" + + [widget.categoriesCloud] + title = "Categories" + +[search] + title = "Cerca" + placeholder = "Tecleja alguna cosa..." + resultTitle = "#PAGES_COUNT pàgines en (#TIME_SECONDS segons)" + +[footer] + builtWith = "Creat amb {{ .Generator }}" + designedBy = "Tema {{ .Theme }} dissenyat per {{ .DesignedBy }}" + +[cookies] + title = "Consentiment de galetes" + text = "Utilitzem galetes per millorar la vostra experiència de navegació i analitzar el trànsit del lloc." + acceptAll = "Acceptar-ho tot" + deny = "Denegar" + managePreferences = "Gestionar preferències" + settingsTitle = "Preferències de galetes" + savePreferences = "Desar preferències" + cancel = "Cancel·lar" + footerLink = "Configuració de galetes" + commentsDisabled = "Els comentaris estan desactivats fins que accepteu les galetes funcionals." + + [cookies.necessary] + title = "Necessàries" + text = "Aquestes galetes són necessàries perquè el lloc web funcioni i no es poden desactivar." + + [cookies.analytics] + title = "Analítiques" + text = "Aquestes galetes ens ajuden a entendre com els visitants interactuen amb el nostre lloc web." + + [cookies.functional] + title = "Funcionals" + text = "Aquestes galetes habiliten funcionalitats com comentaris i contingut incrustat." diff --git a/themes/stack/i18n/cs.toml b/themes/stack/i18n/cs.toml new file mode 100644 index 0000000..4b710d6 --- /dev/null +++ b/themes/stack/i18n/cs.toml @@ -0,0 +1,82 @@ +toggleMenu = "Skrýt menu" +darkMode = "Tmavý režim" + +[list] + section = "Kategorie" + + [list.page] + one = "{{ .Count }} stránka" + other = "{{ .Count }} stránek" + + [list.subsection] + one = "Podkategorie" + other = "Podkategorie" + +[article] + back = "Zpět" + tableOfContents = "Obsah" + relatedContent = "Související" + lastUpdatedOn = "Naposledy aktualizováno" + + [article.readingTime] + one = "{{ .Count }} minuta" + other = "{{ .Count }} minut" + + [article.alert] + note = "Poznámka" + tip = "Tip" + important = "Důležité" + warning = "Varování" + caution = "Upozornění" + + [article.codeblock] + copy = "Kopírovat" + copied = "Zkopírováno!" + +[notFound] + title = "Nenalezeno" + subtitle = "Tato stránka neexistuje" + +[widget] + [widget.archives] + title = "Archivy" + more = "Více" + + [widget.tagCloud] + title = "Štítky" + + [widget.categoriesCloud] + title = "Kategorie" + +[search] + title = "Hledat" + placeholder = "Zadejte něco..." + resultTitle = "#PAGES_COUNT stránek (#TIME_SECONDS sekund)" + +[footer] + builtWith = "Vytvořeno pomocí {{ .Generator }}" + designedBy = "Šablona {{ .Theme }} od {{ .DesignedBy }}" + +[cookies] + title = "Souhlas s cookies" + text = "Používáme cookies ke zlepšení vašeho prohlížení a analýze návštěvnosti webu." + acceptAll = "Přijmout vše" + deny = "Odmítnout" + managePreferences = "Spravovat předvolby" + settingsTitle = "Předvolby cookies" + savePreferences = "Uložit předvolby" + cancel = "Zrušit" + footerLink = "Nastavení cookies" + commentsDisabled = "Komentáře jsou zakázány, dokud nepřijmete funkční cookies." + + [cookies.necessary] + title = "Nutné" + text = "Tyto cookies jsou nutné pro fungování webu a nelze je zakázat." + + [cookies.analytics] + title = "Analytické" + text = "Tyto cookies nám pomáhají pochopit, jak návštěvníci interagují s naším webem." + + [cookies.functional] + title = "Funkční" + text = "Tyto cookies umožňují funkce jako komentáře a vložený obsah." diff --git a/themes/stack/i18n/de.toml b/themes/stack/i18n/de.toml new file mode 100644 index 0000000..b0ee75f --- /dev/null +++ b/themes/stack/i18n/de.toml @@ -0,0 +1,82 @@ +toggleMenu = "Menü umschalten" +darkMode = "Dunkler Modus" + +[list] + section = "Abschnitt" + + [list.page] + one = "{{ .Count }} Seite" + other = "{{ .Count }} Seiten" + + [list.subsection] + one = "Unterabschnitt" + other = "Unterabschnitte" + +[article] + back = "Zurück" + tableOfContents = "Inhaltsverzeichnis" + relatedContent = "Verwandte Inhalte" + lastUpdatedOn = "Zuletzt aktualisiert am" + + [article.readingTime] + one = "{{ .Count }} Minute Lesezeit" + other = "{{ .Count }} Minuten Lesezeit" + + [article.alert] + note = "Hinweis" + tip = "Tipp" + important = "Wichtig" + warning = "Warnung" + caution = "Vorsicht" + + [article.codeblock] + copy = "Kopieren" + copied = "Kopiert!" + +[notFound] + title = "Seite nicht gefunden" + subtitle = "Diese Seite existiert nicht" + +[widget] + [widget.archives] + title = "Archiv" + more = "Weitere" + + [widget.tagCloud] + title = "Schlagwörter" + + [widget.categoriesCloud] + title = "Kategorien" + +[search] + title = "Suche" + placeholder = "Etwas tippen..." + resultTitle = "#PAGES_COUNT Seiten (#TIME_SECONDS Sekunden)" + +[footer] + builtWith = "Erstellt mit {{ .Generator }}" + designedBy = "Theme {{ .Theme }} gestaltet von {{ .DesignedBy }}" + +[cookies] + title = "Cookie-Einwilligung" + text = "Wir verwenden Cookies, um Ihr Surferlebnis zu verbessern und den Website-Traffic zu analysieren." + acceptAll = "Alle akzeptieren" + deny = "Ablehnen" + managePreferences = "Einstellungen verwalten" + settingsTitle = "Cookie-Einstellungen" + savePreferences = "Einstellungen speichern" + cancel = "Abbrechen" + footerLink = "Cookie-Einstellungen" + commentsDisabled = "Kommentare sind deaktiviert, bis Sie funktionale Cookies akzeptieren." + + [cookies.necessary] + title = "Notwendig" + text = "Diese Cookies sind für die Funktion der Website erforderlich und können nicht deaktiviert werden." + + [cookies.analytics] + title = "Analyse" + text = "Diese Cookies helfen uns zu verstehen, wie Besucher mit unserer Website interagieren." + + [cookies.functional] + title = "Funktional" + text = "Diese Cookies ermöglichen Funktionen wie Kommentare und eingebettete Inhalte." diff --git a/themes/stack/i18n/el.toml b/themes/stack/i18n/el.toml new file mode 100644 index 0000000..afcfc8a --- /dev/null +++ b/themes/stack/i18n/el.toml @@ -0,0 +1,79 @@ +toggleMenu = "Εναλλαγή Μενού" +darkMode = "Σκοτεινό θέμα" + +[list] + section = "Ενότητα" + + [list.page] + one = "{{ .Count }} σελιδα" + other = "{{ .Count }} σελιδες" + + [list.subsection] + one = "Υποενότητα" + other = "Υποενότητες" + +[article] + back = "Πισω" + tableOfContents = "Πινακας περιεχομενων" + relatedContent = "Σχετικο περιεχομενο" + lastUpdatedOn = "Τελευταια τροποποιηση στις" + + [article.readingTime] + one = "{{ .Count }} λεπτό ανάγνωσης" + other = "{{ .Count }} λεπτά ανάγνωσης" + + [article.alert] + note = "Σημείωση" + tip = "Συμβουλή" + important = "Σημαντικό" + warning = "Προειδοποίηση" + caution = "Προσοχή" + + [article.codeblock] + copy = "Αντιγραφή" + copied = "Αντιγράφηκε!" + +[notFound] + title = "Δε βρέθηκε" + subtitle = "Η σελίδα δε βρέθηκε." + +[widget] + [widget.archives] + title = "Αρχειο" + more = "Περισσότερα" + + [widget.tagCloud] + title = "Tags" + +[search] + title = "Αναζήτηση" + placeholder = "Πληκτρολογήστε κάτι..." + resultTitle = "#PAGES_COUNT σελιδες (#TIME_SECONDS δευτερολεπτα)" + +[footer] + builtWith = "Δημιουργήθηκε με τη χρήση {{ .Generator }}" + designedBy = "Το θέμα {{ .Theme }} σχεδιάστηκε από το {{ .DesignedBy }}" + +[cookies] + title = "Συγκατάθεση για cookies" + text = "Χρησιμοποιούμε cookies για να βελτιώσουμε την εμπειρία περιήγησής σας και να αναλύσουμε την επισκεψιμότητα του ιστότοπου." + acceptAll = "Αποδοχή όλων" + deny = "Άρνηση" + managePreferences = "Διαχείριση προτιμήσεων" + settingsTitle = "Προτιμήσεις cookies" + savePreferences = "Αποθήκευση προτιμήσεων" + cancel = "Ακύρωση" + footerLink = "Ρυθμίσεις cookies" + commentsDisabled = "Τα σχόλια είναι απενεργοποιημένα μέχρι να αποδεχτείτε τα λειτουργικά cookies." + + [cookies.necessary] + title = "Απαραίτητα" + text = "Αυτά τα cookies είναι απαραίτητα για τη λειτουργία του ιστότοπου και δεν μπορούν να απενεργοποιηθούν." + + [cookies.analytics] + title = "Αναλυτικά" + text = "Αυτά τα cookies μας βοηθούν να κατανοήσουμε πώς οι επισκέπτες αλληλεπιδρούν με τον ιστότοπό μας." + + [cookies.functional] + title = "Λειτουργικά" + text = "Αυτά τα cookies ενεργοποιούν λειτουργίες όπως σχόλια και ενσωματωμένο περιεχόμενο." diff --git a/themes/stack/i18n/en.toml b/themes/stack/i18n/en.toml new file mode 100644 index 0000000..409288e --- /dev/null +++ b/themes/stack/i18n/en.toml @@ -0,0 +1,87 @@ +toggleMenu = "Toggle Menu" +darkMode = "Dark Mode" + +[list] + section = "Section" + + [list.page] + one = "{{ .Count }} page" + other = "{{ .Count }} pages" + + [list.subsection] + one = "Subsection" + other = "Subsections" + +[article] + back = "Back" + tableOfContents = "Table of contents" + relatedContent = "Related content" + lastUpdatedOn = "Last updated on" + + [article.readingTime] + one = "{{ .Count }} minute read" + other = "{{ .Count }} minutes read" + + [article.alert] + note = "Note" + tip = "Tip" + important = "Important" + warning = "Warning" + caution = "Caution" + + [article.codeblock] + copy = "Copy" + copied = "Copied!" + +[notFound] + title = "Not Found" + subtitle = "This page does not exist" + +[widget] + [widget.archives] + title = "Archives" + more = "More" + + [widget.tagCloud] + title = "Tags" + + [widget.categoriesCloud] + title = "Categories" + +[search] + title = "Search" + placeholder = "Type something..." + resultTitle = "#PAGES_COUNT pages (#TIME_SECONDS seconds)" + +[footer] + builtWith = "Built with {{ .Generator }}" + designedBy = "Theme {{ .Theme }} designed by {{ .DesignedBy }}" + +[cookies] + title = "Cookie Consent" + text = "We use cookies to enhance your browsing experience and analyze site traffic." + acceptAll = "Accept All" + deny = "Deny" + managePreferences = "Manage preferences" + settingsTitle = "Cookie Preferences" + savePreferences = "Save preferences" + cancel = "Cancel" + footerLink = "Cookie settings" + commentsDisabled = "Comments are disabled until you accept functional cookies." + + [cookies.necessary] + title = "Necessary" + text = "These cookies are required for the website to function and cannot be disabled." + + [cookies.analytics] + title = "Analytics" + text = "These cookies help us understand how visitors interact with our website." + + [cookies.functional] + title = "Functional" + text = "These cookies enable features like comments and embedded content." + +[pagination] + jumpToPage = "Jump to page" + jump = "Go" + pressEnter = "Press Enter to jump" diff --git a/themes/stack/i18n/es.toml b/themes/stack/i18n/es.toml new file mode 100644 index 0000000..f418abe --- /dev/null +++ b/themes/stack/i18n/es.toml @@ -0,0 +1,82 @@ +toggleMenu = "Ocultar menú" +darkMode = "Modo oscuro" + +[list] + section = "Sección" + + [list.page] + one = "{{ .Count }} página" + other = "{{ .Count }} páginas" + + [list.subsection] + one = "Subsección" + other = "Subsecciones" + +[article] + back = "Volver" + tableOfContents = "Tabla de contenido" + relatedContent = "Contenidos relacionados" + lastUpdatedOn = "Última actualización" + + [article.readingTime] + one = "Tiempo de lectura {{ .Count }} minuto" + other = "Tiempo de lectura {{ .Count }} minutos" + + [article.alert] + note = "Nota" + tip = "Consejo" + important = "Importante" + warning = "Advertencia" + caution = "Precaución" + + [article.codeblock] + copy = "Copiar" + copied = "¡Copiado!" + +[notFound] + title = "No Encontrado" + subtitle = "Esta página no existe" + +[widget] + [widget.archives] + title = "Archivo" + more = "Más" + + [widget.tagCloud] + title = "Etiquetas" + + [widget.categoriesCloud] + title = "Categorías" + +[search] + title = "Búsqueda" + placeholder = "Escribe algo..." + resultTitle = "#PAGES_COUNT páginas en (#TIME_SECONDS segundos)" + +[footer] + builtWith = "Creado con {{ .Generator }}" + designedBy = "Tema {{ .Theme }} diseñado por {{ .DesignedBy }}" + +[cookies] + title = "Consentimiento de cookies" + text = "Utilizamos cookies para mejorar su experiencia de navegación y analizar el tráfico del sitio." + acceptAll = "Aceptar todo" + deny = "Rechazar" + managePreferences = "Gestionar preferencias" + settingsTitle = "Preferencias de cookies" + savePreferences = "Guardar preferencias" + cancel = "Cancelar" + footerLink = "Configuración de cookies" + commentsDisabled = "Los comentarios están desactivados hasta que acepte las cookies funcionales." + + [cookies.necessary] + title = "Necesarias" + text = "Estas cookies son necesarias para el funcionamiento del sitio web y no se pueden desactivar." + + [cookies.analytics] + title = "Analíticas" + text = "Estas cookies nos ayudan a entender cómo los visitantes interactúan con nuestro sitio web." + + [cookies.functional] + title = "Funcionales" + text = "Estas cookies permiten funciones como comentarios y contenido incrustado." diff --git a/themes/stack/i18n/fa.toml b/themes/stack/i18n/fa.toml new file mode 100644 index 0000000..40b3097 --- /dev/null +++ b/themes/stack/i18n/fa.toml @@ -0,0 +1,82 @@ +toggleMenu = "منو" +darkMode = "حالت شب" + +[list] + section = "بخش" + + [list.page] + one = "{{ .Count }} صفحه" + other = "{{ .Count }} صفحه" + + [list.subsection] + one = "زیربخش" + other = "زیربخش" + +[article] + back = "قبلی" + tableOfContents = "فهرست" + relatedContent = "مطالب مرتبط" + lastUpdatedOn = "آخرین بروزرسانی در" + + [article.readingTime] + one = "مطالعه در {{ .Count }} دقیقه" + other = "مطالعه در {{ .Count }} دقیقه" + + [article.alert] + note = "یادداشت" + tip = "نکته" + important = "مهم" + warning = "هشدار" + caution = "احتیاط" + + [article.codeblock] + copy = "کپی" + copied = "کپی شد!" + +[notFound] + title = "یافت نشد" + subtitle = "این صحه وجود ندارد" + +[widget] + [widget.archives] + title = "آرشیو" + more = "بیشتر" + + [widget.tagCloud] + title = "تگ ها" + + [widget.categoriesCloud] + title = "دسته بندی" + +[search] + title = "جستجو" + placeholder = "تایپ کنید ..." + resultTitle = "#PAGES_COUNT صفحه (#TIME_SECONDS ثانیه)" + +[footer] + builtWith = "قدرت گرفته از {{ .Generator }}" + designedBy = "قالب {{ .Theme }} ساخته شده توسط {{ .DesignedBy }}" + +[cookies] + title = "رضایت کوکی" + text = "ما از کوکی‌ها برای بهبود تجربه مرورگری شما و تجزیه و تحلیل ترافیک سایت استفاده می‌کنیم." + acceptAll = "پذیرش همه" + deny = "رد کردن" + managePreferences = "مدیریت تنظیمات" + settingsTitle = "تنظیمات کوکی" + savePreferences = "ذخیره تنظیمات" + cancel = "لغو" + footerLink = "تنظیمات کوکی" + commentsDisabled = "نظرات تا زمانی که کوکی‌های کاربردی را بپذیرید غیرفعال هستند." + + [cookies.necessary] + title = "ضروری" + text = "این کوکی‌ها برای عملکرد وب‌سایت الزامی هستند و نمی‌توان آنها را غیرفعال کرد." + + [cookies.analytics] + title = "تجزیه و تحلیل" + text = "این کوکی‌ها به ما کمک می‌کنند تا بفهمیم بازدیدکنندگان چگونه با وب‌سایت ما تعامل دارند." + + [cookies.functional] + title = "کاربردی" + text = "این کوکی‌ها ویژگی‌هایی مانند نظرات و محتوای جاسازی‌شده را فعال می‌کنند." diff --git a/themes/stack/i18n/fr.toml b/themes/stack/i18n/fr.toml new file mode 100644 index 0000000..ff99471 --- /dev/null +++ b/themes/stack/i18n/fr.toml @@ -0,0 +1,82 @@ +toggleMenu = "Afficher le menu" +darkMode = "Mode sombre" + +[list] + section = "Section" + + [list.page] + one = "{{ .Count }} page" + other = "{{ .Count }} pages" + + [list.subsection] + one = "Sous-section" + other = "Sous-sections" + +[article] + back = "Retour" + tableOfContents = "Table des matières" + relatedContent = "Contenus liés" + lastUpdatedOn = "Dernière mise à jour le" + + [article.readingTime] + one = "{{ .Count }} minute de lecture" + other = "{{ .Count }} minutes de lecture" + + [article.alert] + note = "Note" + tip = "Astuce" + important = "Important" + warning = "Avertissement" + caution = "Attention" + + [article.codeblock] + copy = "Copier" + copied = "Copié !" + +[notFound] + title = "Page non trouvée" + subtitle = "Cette page n'existe pas." + +[widget] + [widget.archives] + title = "Archives" + more = "Autres" + + [widget.tagCloud] + title = "Mots clés" + + [widget.categoriesCloud] + title = "Catégories" + +[search] + title = "Rechercher" + placeholder = "Cherchez un article, une publication, etc." + resultTitle = "#PAGES_COUNT pages (#TIME_SECONDS secondes)" + +[footer] + builtWith = "Généré avec {{ .Generator }}" + designedBy = "Thème {{ .Theme }} conçu par {{ .DesignedBy }}" + +[cookies] + title = "Consentement aux cookies" + text = "Nous utilisons des cookies pour améliorer votre expérience de navigation et analyser le trafic du site." + acceptAll = "Tout accepter" + deny = "Refuser" + managePreferences = "Gérer les préférences" + settingsTitle = "Préférences de cookies" + savePreferences = "Enregistrer les préférences" + cancel = "Annuler" + footerLink = "Paramètres des cookies" + commentsDisabled = "Les commentaires sont désactivés jusqu'à ce que vous acceptiez les cookies fonctionnels." + + [cookies.necessary] + title = "Nécessaires" + text = "Ces cookies sont nécessaires au fonctionnement du site et ne peuvent pas être désactivés." + + [cookies.analytics] + title = "Analytiques" + text = "Ces cookies nous aident à comprendre comment les visiteurs interagissent avec notre site." + + [cookies.functional] + title = "Fonctionnels" + text = "Ces cookies permettent des fonctionnalités comme les commentaires et le contenu intégré." diff --git a/themes/stack/i18n/hi.toml b/themes/stack/i18n/hi.toml new file mode 100644 index 0000000..02ddc74 --- /dev/null +++ b/themes/stack/i18n/hi.toml @@ -0,0 +1,82 @@ +toggleMenu = "मेनू टॉगल करें" +darkMode = "डार्क मोड" + +[list] + section = "अनुभाग" + + [list.page] + one = "{{ .Count }} पेज" + other = "{{ .Count }} पेज" + + [list.subsection] + one = "उपधारा" + other = "उपखंड" + +[article] + back = "पीछे" + tableOfContents = "विषयसूची" + relatedContent = "संबंधित सामग्री" + lastUpdatedOn = "अंतिम बार अपडेट किया गया" + + [article.readingTime] + one = "{{ .Count }} मिनट पढ़ें" + other = "{{ .Count }} मिनट पढ़ें" + + [article.alert] + note = "नोट" + tip = "सलाह" + important = "महत्वपूर्ण" + warning = "चेतावनी" + caution = "सावधानी" + + [article.codeblock] + copy = "कॉपी" + copied = "कॉपी हो गया!" + +[notFound] + title = "404 नहीं मिला।" + subtitle = "यह पृष्ठ मौजूद नहीं है।" + +[widget] + [widget.archives] + title = "अभिलेखागार" + more = "अधिक" + + [widget.tagCloud] + title = "टैग" + + [widget.categoriesCloud] + title = "श्रेणियाँ" + +[search] + title = "खोज" + placeholder = "कुछ लिखें..." + resultTitle = "#PAGES_COUNT पेज (#TIME_SECONDS सेकंड)" + +[footer] + builtWith = "निर्मित {{ .Generator }} के साथ" + designedBy = "थीम {{ .Theme }} द्वारा डिज़ाइन किया गया {{ .DesignedBy }}" + +[cookies] + title = "कुकी सहमति" + text = "हम आपके ब्राउज़िंग अनुभव को बेहतर बनाने और साइट ट्रैफ़िक का विश्लेषण करने के लिए कुकीज़ का उपयोग करते हैं।" + acceptAll = "सभी स्वीकार करें" + deny = "अस्वीकार करें" + managePreferences = "वरीयताएँ प्रबंधित करें" + settingsTitle = "कुकी वरीयताएँ" + savePreferences = "वरीयताएँ सहेजें" + cancel = "रद्द करें" + footerLink = "कुकी सेटिंग्स" + commentsDisabled = "जब तक आप कार्यात्मक कुकीज़ स्वीकार नहीं करते हैं, टिप्पणियाँ अक्षम हैं।" + + [cookies.necessary] + title = "आवश्यक" + text = "ये कुकीज़ वेबसाइट के कार्य के लिए आवश्यक हैं और इन्हें अक्षम नहीं किया जा सकता।" + + [cookies.analytics] + title = "विश्लेषण" + text = "ये कुकीज़ हमें यह समझने में मदद करती हैं कि विज़िटर हमारी वेबसाइट के साथ कैसे इंटरैक्ट करते हैं।" + + [cookies.functional] + title = "कार्यात्मक" + text = "ये कुकीज़ टिप्पणियों और एम्बेडेड सामग्री जैसी सुविधाओं को सक्षम करती हैं." diff --git a/themes/stack/i18n/hu.toml b/themes/stack/i18n/hu.toml new file mode 100644 index 0000000..2ed7273 --- /dev/null +++ b/themes/stack/i18n/hu.toml @@ -0,0 +1,82 @@ +toggleMenu = "Menü Kapcsolása" +darkMode = "Sötét Mód" + +[list] + section = "Szekció" + + [list.page] + one = "{{ .Count }} oldal" + other = "{{ .Count }} oldalak" + + [list.subsection] + one = "Alszekció" + other = "Alszekciók" + +[article] + back = "Vissza" + tableOfContents = "Tartalomjegyzék" + relatedContent = "Kapcsolódó tartalom" + lastUpdatedOn = "Utolsó frissítés időpontja" + + [article.readingTime] + one = "{{ .Count }} percnyi olvasmány" + other = "{{ .Count }} percnyi olvasmány" + + [article.alert] + note = "Megjegyzés" + tip = "Tipp" + important = "Fontos" + warning = "Figyelmeztetés" + caution = "Óvatosság" + + [article.codeblock] + copy = "Másolás" + copied = "Másolva!" + +[notFound] + title = "Nem található" + subtitle = "Ez az oldal nem létezik" + +[widget] + [widget.archives] + title = "Archívum" + more = "Több" + + [widget.tagCloud] + title = "Cimkék" + + [widget.categoriesCloud] + title = "Kategóriák" + +[search] + title = "Keresés" + placeholder = "Írj valamit..." + resultTitle = "#PAGES_COUNT oldal (#TIME_SECONDS másodperc alatt)" + +[footer] + builtWith = "{{ .Generator }} használatával készült" + designedBy = "A {{ .Theme }} dizájnt {{ .DesignedBy }} tervezte" + +[cookies] + title = "Süti hozzájárulás" + text = "Sütiket használunk a böngészési élmény javítása és a webhely forgalmának elemzése érdekében." + acceptAll = "Összes elfogadása" + deny = "Elutasítás" + managePreferences = "Beállítások kezelése" + settingsTitle = "Süti beállítások" + savePreferences = "Beállítások mentése" + cancel = "Mégse" + footerLink = "Süti beállítások" + commentsDisabled = "A hozzászólások le vannak tiltva, amíg nem fogadja el a funkcionális sütiket." + + [cookies.necessary] + title = "Szükséges" + text = "Ezek a sütik szükségesek a webhely működéséhez és nem lehet őket letiltani." + + [cookies.analytics] + title = "Analitikai" + text = "Ezek a sütik segítenek megérteni, hogyan lépnek kapcsolatba a látogatók a weboldalunkkal." + + [cookies.functional] + title = "Funkcionális" + text = "Ezek a sütik olyan funkciókat tesznek lehetővé, mint a hozzászólások és a beágyazott tartalom." diff --git a/themes/stack/i18n/id.toml b/themes/stack/i18n/id.toml new file mode 100644 index 0000000..9681622 --- /dev/null +++ b/themes/stack/i18n/id.toml @@ -0,0 +1,82 @@ +toggleMenu = "Tampilkan Menu" +darkMode = "Mode Gelap" + +[list] + section = "Bagian" + + [list.page] + one = "{{ .Count }} halaman" + other = "{{ .Count }} halaman" + + [list.subsection] + one = "Subbagian" + other = "Subbagian" + +[article] + back = "Kembali" + tableOfContents = "Daftar Isi" + relatedContent = "Konten terkait" + lastUpdatedOn = "Terakhir diperbarui pada" + + [article.readingTime] + one = "Waktu Membaca: {{ .Count }} menit" + other = "Waktu Membaca: {{ .Count }} menit" + + [article.alert] + note = "Catatan" + tip = "Tips" + important = "Penting" + warning = "Peringatan" + caution = "Perhatian" + + [article.codeblock] + copy = "Salin" + copied = "Disalin!" + +[notFound] + title = "Tidak ditemukan" + subtitle = "Halaman yang Anda akses tidak ditemukan." + +[widget] + [widget.archives] + title = "Arsip" + more = "Lebih" + + [widget.tagCloud] + title = "Tag" + + [widget.categoriesCloud] + title = "Kategori" + +[search] + title = "Cari" + placeholder = "Ketik sesuatu..." + resultTitle = "#PAGES_COUNT halaman (#TIME_SECONDS detik)" + +[footer] + builtWith = "Dibangun dengan {{ .Generator }}" + designedBy = "Tema {{ .Theme }} dirancang oleh {{ .DesignedBy }}" + +[cookies] + title = "Persetujuan Cookie" + text = "Kami menggunakan cookie untuk meningkatkan pengalaman browsing Anda dan menganalisis lalu lintas situs." + acceptAll = "Terima Semua" + deny = "Tolak" + managePreferences = "Kelola preferensi" + settingsTitle = "Preferensi Cookie" + savePreferences = "Simpan preferensi" + cancel = "Batal" + footerLink = "Pengaturan cookie" + commentsDisabled = "Komentar dinonaktifkan sampai Anda menerima cookie fungsional." + + [cookies.necessary] + title = "Diperlukan" + text = "Cookie ini diperlukan agar situs web berfungsi dan tidak dapat dinonaktifkan." + + [cookies.analytics] + title = "Analitik" + text = "Cookie ini membantu kami memahami bagaimana pengunjung berinteraksi dengan situs web kami." + + [cookies.functional] + title = "Fungsional" + text = "Cookie ini mengaktifkan fitur seperti komentar dan konten yang disematkan." diff --git a/themes/stack/i18n/it.toml b/themes/stack/i18n/it.toml new file mode 100644 index 0000000..e9a705a --- /dev/null +++ b/themes/stack/i18n/it.toml @@ -0,0 +1,82 @@ +toggleMenu = "Toggle Menu" +darkMode = "Modalità scura" + +[list] + section = "Sezione" + + [list.page] + one = "{{ .Count }} pagina" + other = "{{ .Count }} pagine" + + [list.subsection] + one = "Sottosezione" + other = "Sottosezioni" + +[article] + back = "Indietro" + tableOfContents = "Indice" + relatedContent = "Contenuti correlati" + lastUpdatedOn = "Aggiornato il" + + [article.readingTime] + one = "{{ .Count }} min per leggere" + other = "{{ .Count }} min per leggere" + + [article.alert] + note = "Nota" + tip = "Suggerimento" + important = "Importante" + warning = "Avvertenza" + caution = "Attenzione" + + [article.codeblock] + copy = "Copia" + copied = "Copiato!" + +[notFound] + title = "Non trovato" + subtitle = "Questa pagina non esiste." + +[widget] + [widget.archives] + title = "Archivi" + more = "Di più" + + [widget.tagCloud] + title = "Tag" + + [widget.categoriesCloud] + title = "Categorie" + +[search] + title = "Cerca" + placeholder = "Scrivi qualcosa..." + resultTitle = "#PAGES_COUNT pagine (#TIME_SECONDS secondi)" + +[footer] + builtWith = "Realizzato con {{ .Generator }}" + designedBy = "Tema {{ .Theme }} realizzato da {{ .DesignedBy }}" + +[cookies] + title = "Consenso ai cookie" + text = "Utilizziamo i cookie per migliorare la tua esperienza di navigazione e analizzare il traffico del sito." + acceptAll = "Accetta tutti" + deny = "Rifiuta" + managePreferences = "Gestisci preferenze" + settingsTitle = "Preferenze cookie" + savePreferences = "Salva preferenze" + cancel = "Annulla" + footerLink = "Impostazioni cookie" + commentsDisabled = "I commenti sono disabilitati finché non accetti i cookie funzionali." + + [cookies.necessary] + title = "Necessari" + text = "Questi cookie sono necessari per il funzionamento del sito web e non possono essere disattivati." + + [cookies.analytics] + title = "Analitici" + text = "Questi cookie ci aiutano a capire come i visitatori interagiscono con il nostro sito web." + + [cookies.functional] + title = "Funzionali" + text = "Questi cookie abilitano funzionalità come commenti e contenuti incorporati." diff --git a/themes/stack/i18n/ja.toml b/themes/stack/i18n/ja.toml new file mode 100644 index 0000000..5ef680d --- /dev/null +++ b/themes/stack/i18n/ja.toml @@ -0,0 +1,78 @@ +toggleMenu = "メニューを開く・閉じる" +darkMode = "ダークモード" + +[list] + page = "{{ .Count }} ページ目" + section = "セクション" + subsection = "サブセクション" + +[article] + back = "前のページ" + tableOfContents = "目次" + relatedContent = "関連するコンテンツ" + lastUpdatedOn = "最終更新" + readingTime = "読了時間: {{ .Count }}分" + + [article.alert] + note = "注記" + tip = "ヒント" + important = "重要" + warning = "警告" + caution = "注意" + + [article.codeblock] + copy = "コピー" + copied = "コピーしました!" + +[notFound] + title = "404 Not Found" + subtitle = "指定されたページは存在しません。" + +[widget] + [widget.archives] + title = "アーカイブ" + more = "さらに見る" + + [widget.tagCloud] + title = "タグ" + + [widget.categoriesCloud] + title = "カテゴリ" + +[search] + title = "検索" + placeholder = "入力..." + resultTitle = "#PAGES_COUNT 件 (#TIME_SECONDS 秒)" + +[footer] + builtWith = "{{ .Generator }} で構築されています。" + designedBy = "テーマ {{ .Theme }} は {{ .DesignedBy }} によって設計されています。" + +[cookies] + title = "Cookieの同意" + text = "ブラウジング体験を向上させ、サイトトラフィックを分析するためにCookieを使用しています。" + acceptAll = "すべて受け入れる" + deny = "拒否" + managePreferences = "設定を管理" + settingsTitle = "Cookie設定" + savePreferences = "設定を保存" + cancel = "キャンセル" + footerLink = "Cookie設定" + commentsDisabled = "機能的なCookieを受け入れるまで、コメントは無効になっています。" + + [cookies.necessary] + title = "必須" + text = "これらのCookieはウェブサイトの機能に必要であり、無効にすることはできません。" + + [cookies.analytics] + title = "分析" + text = "これらのCookieは、訪問者が当サイトとどのように対話するかを理解するのに役立ちます。" + + [cookies.functional] + title = "機能的" + text = "これらのCookieは、コメントや埋め込みコンテンツなどの機能を有効にします." + +[pagination] + jumpToPage = "ページにジャンプ" + jump = "ジャンプ" + pressEnter = "Enter キーでジャンプ" diff --git a/themes/stack/i18n/ko.toml b/themes/stack/i18n/ko.toml new file mode 100644 index 0000000..9ad984d --- /dev/null +++ b/themes/stack/i18n/ko.toml @@ -0,0 +1,82 @@ +toggleMenu = "메뉴 여닫기" +darkMode = "다크 모드" + +[list] + section = "섹션" + + [list.page] + one = "{{ .Count }} 페이지" + other = "{{ .Count }} 페이지" + + [list.subsection] + one = "서브섹션" + other = "서브섹션" + +[article] + back = "뒤로가기" + tableOfContents = "목차" + relatedContent = "관련 글" + lastUpdatedOn = "마지막 수정: " + + [article.readingTime] + one = "{{ .Count }} 분 정도" + other = "{{ .Count }} 분 정도" + + [article.alert] + note = "참고" + tip = "팁" + important = "중요" + warning = "경고" + caution = "주의" + + [article.codeblock] + copy = "복사" + copied = "복사됨!" + +[notFound] + title = "찾을 수 없음" + subtitle = "페이지를 찾을 수 없습니다." + +[widget] + [widget.archives] + title = "보관함" + more = "더보기" + + [widget.categoriesCloud] + title = "카테고리" + + [widget.tagCloud] + title = "태그" + +[search] + title = "검색" + placeholder = "검색어를 입력하세요..." + resultTitle = "#PAGES_COUNT 페이지 (#TIME_SECONDS 초)" + +[footer] + builtWith = "{{ .Generator }}로 만듦" + designedBy = "{{ .DesignedBy }}의 {{ .Theme }} 테마 사용 중" + +[cookies] + title = "쿠키 동의" + text = "브라우징 경험을 향상시키고 사이트 트래픽을 분석하기 위해 쿠키를 사용합니다." + acceptAll = "모두 허용" + deny = "거부" + managePreferences = "설정 관리" + settingsTitle = "쿠키 설정" + savePreferences = "설정 저장" + cancel = "취소" + footerLink = "쿠키 설정" + commentsDisabled = "기능적 쿠키를 수락할 때까지 댓글이 비활성화됩니다." + + [cookies.necessary] + title = "필수" + text = "이 쿠키는 웹사이트 기능에 필요하며 비활성화할 수 없습니다." + + [cookies.analytics] + title = "분석" + text = "이 쿠키는 방문자가 웹사이트와 상호작용하는 방식을 이해하는 데 도움이 됩니다." + + [cookies.functional] + title = "기능적" + text = "이 쿠키는 댓글 및 임베디드 콘텐츠와 같은 기능을 활성화합니다." diff --git a/themes/stack/i18n/nl.toml b/themes/stack/i18n/nl.toml new file mode 100644 index 0000000..a803d41 --- /dev/null +++ b/themes/stack/i18n/nl.toml @@ -0,0 +1,77 @@ +toggleMenu = "Open Menu" +darkMode = "Donkere modus" + +[list] + section = "Sectie" + + [list.page] + one = "{{ .Count }} pagina" + other = "{{ .Count }} pagina's" + + [list.subsection] + one = "Subsectie" + other = "Subsecties" + +[article] + relatedContent = "Gerelateerde inhoud" + lastUpdatedOn = "Laatst bijgewerkt op" + readingTime = "{{ .Count }} leestijd" + + [article.alert] + note = "Opmerking" + tip = "Tip" + important = "Belangrijk" + warning = "Waarschuwing" + caution = "Let op" + + [article.codeblock] + copy = "Kopiëren" + copied = "Gekopieerd!" + +[notFound] + title = "Niet gevonden" + subtitle = "Deze pagina bestaat niet." + +[widget] + [widget.archives] + title = "Archief" + more = "Meer" + + [widget.tagCloud] + title = "Tags" + + [widget.categoriesCloud] + title = "Categorie" + +[search] + title = "Zoeken" + placeholder = "Typ iets" + resultTitle = "#PAGES_COUNT pagina's (#TIME_SECONDS seconden)" + +[footer] + builtWith = "Gemaakt met {{ .Generator }}" + designedBy = "Theme {{ .Theme }} ontworpen door {{ .DesignedBy }}" + +[cookies] + title = "Cookie-toestemming" + text = "We gebruiken cookies om uw browse-ervaring te verbeteren en siteverkeer te analyseren." + acceptAll = "Accepteer alles" + deny = "Weigeren" + managePreferences = "Voorkeuren beheren" + settingsTitle = "Cookie-voorkeuren" + savePreferences = "Voorkeuren opslaan" + cancel = "Annuleren" + footerLink = "Cookie-instellingen" + commentsDisabled = "Reacties zijn uitgeschakeld totdat u functionele cookies accepteert." + + [cookies.necessary] + title = "Noodzakelijk" + text = "Deze cookies zijn vereist voor de werking van de website en kunnen niet worden uitgeschakeld." + + [cookies.analytics] + title = "Analyse" + text = "Deze cookies helpen ons begrijpen hoe bezoekers omgaan met onze website." + + [cookies.functional] + title = "Functioneel" + text = "Deze cookies maken functies mogelijk zoals reacties en ingesloten inhoud." diff --git a/themes/stack/i18n/oc.toml b/themes/stack/i18n/oc.toml new file mode 100644 index 0000000..c9ed85f --- /dev/null +++ b/themes/stack/i18n/oc.toml @@ -0,0 +1,82 @@ +toggleMenu = "Alternar menú" +darkMode = "Mòde fosc" + +[list] + section = "Seccion" + + [list.page] + one = "{{ .Count }} pagina" + other = "{{ .Count }} paginas" + + [list.subsection] + one = "Josseccion" + other = "Josseccions" + +[article] + back = "Tornar" + tableOfContents = "Taula de contengut" + relatedContent = "Contenguts relacionats" + lastUpdatedOn = "Darrièra actualizacion" + + [article.readingTime] + one = "{{ .Count }} minuta de lectura" + other = "{{ .Count }} minutas de lectura" + + [article.alert] + note = "Nòta" + tip = "Conselh" + important = "Important" + warning = "Avertiment" + caution = "Atencion" + + [article.codeblock] + copy = "Copiar" + copied = "Copiat!" + +[notFound] + title = "Non trobat" + subtitle = "Aquesta pagina existís pas" + +[widget] + [widget.archives] + title = "Archiu" + more = "Mai" + + [widget.tagCloud] + title = "Etiquetas" + + [widget.categoriesCloud] + title = "Categorias" + +[search] + title = "Cercar" + placeholder = "Picatz quicòm..." + resultTitle = "#PAGES_COUNT paginas dins (#TIME_SECONDS segons)" + +[footer] + builtWith = "Creat amb {{ .Generator }}" + designedBy = "Tàma {{ .Theme }} concebut per {{ .DesignedBy }}" + +[cookies] + title = "Consentiment de cookies" + text = "Utilizem cookies per melhorar vòstra experiéncia de navegacion e analisar lo trafic del site." + acceptAll = "Acceptar tot" + deny = "Refusar" + managePreferences = "Gerir las preferéncias" + settingsTitle = "Preferéncias de cookies" + savePreferences = "Enregistrar las preferéncias" + cancel = "Anullar" + footerLink = "Paramètres de cookies" + commentsDisabled = "Los comentaris son desactivats fins qu'acceptatz las cookies foncionalas." + + [cookies.necessary] + title = "Necessaris" + text = "Aquestes cookies son necessàrias pel foncionament del site web e se pòdon pas desactivar." + + [cookies.analytics] + title = "Analiticas" + text = "Aquestes cookies nos ajudan a comprendre cossí los visitaires interagisson amb nòstre site web." + + [cookies.functional] + title = "Foncionalas" + text = "Aquestes cookies activan de foncionalitats coma los comentaris e lo contengut incorporat." diff --git a/themes/stack/i18n/pl.toml b/themes/stack/i18n/pl.toml new file mode 100644 index 0000000..c046a6b --- /dev/null +++ b/themes/stack/i18n/pl.toml @@ -0,0 +1,93 @@ +toggleMenu = "Przełącz Menu" +darkMode = "Tryb ciemny" + +[list] + section = "Sekcja" + + [list.page] + one = "{{ .Count }} strona" + few = "{{ .Count }} strony" + many = "{{ .Count }} stron" + other = "{{ .Count }} stron" + + [list.subsection] + one = "Podsekcja" + few = "Podsekcje" + many = "Podsekcji" + other = "Podsekcji" + +[article] + back = "Wróć" + tableOfContents = "Spis treści" + relatedContent = "Powiązane artykuły" + lastUpdatedOn = "Ostatnio zaktualizowany" + + [article.readingTime] + one = "Przeczytasz w {{ .Count }} minutę" + few = "Przeczytasz w {{ .Count }} minuty" + many = "Przeczytasz w {{ .Count }} minut" + other = "Przeczytasz w {{ .Count }} minut" + + [article.alert] + note = "Notatka" + tip = "Wskazówka" + important = "Ważne" + warning = "Ostrzeżenie" + caution = "Uwaga" + + [article.codeblock] + copy = "Kopiuj" + copied = "Skopiowano!" + +[notFound] + title = "Nie znaleziono" + subtitle = "Ta strona nie istnieje" + +[widget] + [widget.archives] + title = "Archiwum" + more = "Więcej" + + [widget.tagCloud] + title = "Tagi" + + [widget.categoriesCloud] + title = "Kategorie" + +[search] + title = "Szukaj" + placeholder = "Wpisz coś..." + + [search.resultTitle] + one = "#PAGES_COUNT strona (#TIME_SECONDS sekund)" + few = "#PAGES_COUNT strony (#TIME_SECONDS sekund)" + many = "#PAGES_COUNT stron (#TIME_SECONDS sekund)" + other = "#PAGES_COUNT stron (#TIME_SECONDS sekund)" + +[footer] + builtWith = "Zbudowano z {{ .Generator }}" + designedBy = "Motyw {{ .Theme }} zaprojektowany przez {{ .DesignedBy }}" + +[cookies] + title = "Zgoda na pliki cookie" + text = "Używamy plików cookie, aby poprawić Twoje wrażenia z przeglądania i analizować ruch w witrynie." + acceptAll = "Akceptuj wszystkie" + deny = "Odrzuć" + managePreferences = "Zarządzaj preferencjami" + settingsTitle = "Preferencje plików cookie" + savePreferences = "Zapisz preferencje" + cancel = "Anuluj" + footerLink = "Ustawienia plików cookie" + commentsDisabled = "Komentarze są wyłączone, dopóki nie zaakceptujesz funkcjonalnych plików cookie." + + [cookies.necessary] + title = "Niezbędne" + text = "Te pliki cookie są wymagane do działania witryny i nie można ich wyłączyć." + + [cookies.analytics] + title = "Analityczne" + text = "Te pliki cookie pomagają nam zrozumieć, jak odwiedzający wchodzą w interakcję z naszą witryną." + + [cookies.functional] + title = "Funkcjonalne" + text = "Te pliki cookie włączają funkcje takie jak komentarze i osadzone treści." diff --git a/themes/stack/i18n/pt-br.toml b/themes/stack/i18n/pt-br.toml new file mode 100644 index 0000000..86650f9 --- /dev/null +++ b/themes/stack/i18n/pt-br.toml @@ -0,0 +1,82 @@ +toggleMenu = "Alternar Menu" +darkMode = "Modo Escuro" + +[list] + section = "Seção" + + [list.page] + one = "{{ .Count }} página" + other = "{{ .Count }} páginas" + + [list.subsection] + one = "Subseção" + other = "Subseções" + +[article] + back = "Voltar" + tableOfContents = "Índice" + relatedContent = "Conteúdo relacionado" + lastUpdatedOn = "Última atualização em" + + [article.readingTime] + one = "{{ .Count }} minuto de leitura" + other = "{{ .Count }} minutos de leitura" + + [article.alert] + note = "Nota" + tip = "Dica" + important = "Importante" + warning = "Aviso" + caution = "Cuidado" + + [article.codeblock] + copy = "Copiar" + copied = "Copiado!" + +[notFound] + title = "Não Encontrado" + subtitle = "Esta página não existe." + +[widget] + [widget.archives] + title = "Arquivos" + more = "Mais" + + [widget.tagCloud] + title = "Tags" + + [widget.categoriesCloud] + title = "Categorias" + +[search] + title = "Busca" + placeholder = "Digite algo..." + resultTitle = "#PAGES_COUNT páginas (#TIME_SECONDS segundos)" + +[footer] + builtWith = "Criado com {{ .Generator }}" + designedBy = "Tema {{ .Theme }} desenvolvido por {{ .DesignedBy }}" + +[cookies] + title = "Consentimento de cookies" + text = "Usamos cookies para melhorar sua experiência de navegação e analisar o tráfego do site." + acceptAll = "Aceitar todos" + deny = "Recusar" + managePreferences = "Gerenciar preferências" + settingsTitle = "Preferências de cookies" + savePreferences = "Salvar preferências" + cancel = "Cancelar" + footerLink = "Configurações de cookies" + commentsDisabled = "Os comentários estão desativados até que você aceite os cookies funcionais." + + [cookies.necessary] + title = "Necessários" + text = "Estes cookies são necessários para o funcionamento do site e não podem ser desativados." + + [cookies.analytics] + title = "Analíticos" + text = "Estes cookies nos ajudam a entender como os visitantes interagem com nosso site." + + [cookies.functional] + title = "Funcionais" + text = "Estes cookies habilitam recursos como comentários e conteúdo incorporado." diff --git a/themes/stack/i18n/pt-pt.toml b/themes/stack/i18n/pt-pt.toml new file mode 100644 index 0000000..377ef0b --- /dev/null +++ b/themes/stack/i18n/pt-pt.toml @@ -0,0 +1,82 @@ +toggleMenu = "Alternar Menu" +darkMode = "Modo Escuro" + +[list] + section = "Secção" + + [list.page] + one = "{{ .Count }} página" + other = "{{ .Count }} páginas" + + [list.subsection] + one = "Subsecção" + other = "Subsecções" + +[article] + back = "Voltar" + tableOfContents = "Índice" + relatedContent = "Conteúdo relacionado" + lastUpdatedOn = "Última atualização a" + + [article.readingTime] + one = "{{ .Count }} minuto de leitura" + other = "{{ .Count }} minutos de leitura" + + [article.alert] + note = "Nota" + tip = "Dica" + important = "Importante" + warning = "Aviso" + caution = "Cuidado" + + [article.codeblock] + copy = "Copiar" + copied = "Copiado!" + +[notFound] + title = "Não Encontrado" + subtitle = "Esta página não existe." + +[widget] + [widget.archives] + title = "Arquivos" + more = "Mais" + + [widget.tagCloud] + title = "Tags" + + [widget.categoriesCloud] + title = "Categorias" + +[search] + title = "Pesquisa" + placeholder = "Escreva algo..." + resultTitle = "#PAGES_COUNT páginas (#TIME_SECONDS segundos)" + +[footer] + builtWith = "Criado com {{ .Generator }}" + designedBy = "Tema {{ .Theme }} desenvolvido por {{ .DesignedBy }}" + +[cookies] + title = "Consentimento de cookies" + text = "Utilizamos cookies para melhorar a sua experiência de navegação e analisar o tráfego do site." + acceptAll = "Aceitar todos" + deny = "Recusar" + managePreferences = "Gerir preferências" + settingsTitle = "Preferências de cookies" + savePreferences = "Guardar preferências" + cancel = "Cancelar" + footerLink = "Definições de cookies" + commentsDisabled = "Os comentários estão desativados até que aceite os cookies funcionais." + + [cookies.necessary] + title = "Necessários" + text = "Estes cookies são necessários para o funcionamento do website e não podem ser desativados." + + [cookies.analytics] + title = "Analíticos" + text = "Estes cookies ajudam-nos a compreender como os visitantes interagem com o nosso website." + + [cookies.functional] + title = "Funcionais" + text = "Estes cookies permitem funcionalidades como comentários e conteúdo incorporado." diff --git a/themes/stack/i18n/ru.toml b/themes/stack/i18n/ru.toml new file mode 100644 index 0000000..618eb3b --- /dev/null +++ b/themes/stack/i18n/ru.toml @@ -0,0 +1,83 @@ +toggleMenu = "Показать/скрыть меню" +darkMode = "Тёмный режим" + +[list] + section = "Раздел" + + [list.page] + one = "{{ .Count }} страница" + few = "{{ .Count }} страницы" + many = "{{ .Count }} страниц" + other = "{{ .Count }} страниц" + + [list.subsection] + one = "Подраздел" + few = "Подразделы" + many = "Подразделы" + other = "Подразделы" + +[article] + back = "Назад" + relatedContent = "Также рекомендуем" + lastUpdatedOn = "Обновлено" + tableOfContents = "Содержание" + readingTime = "Время чтения: {{ .Count }} мин." + + [article.alert] + note = "Примечание" + tip = "Совет" + important = "Важно" + warning = "Предупреждение" + caution = "Осторожно" + + [article.codeblock] + copy = "Копировать" + copied = "Скопировано!" + +[notFound] + title = "Не найдено" + subtitle = "Запрашиваемая страница не существует" + +[widget] + [widget.archives] + title = "Архивы" + more = "Ещё" + + [widget.tagCloud] + title = "Теги" + + [widget.categoriesCloud] + title = "Категории" + +[search] + title = "Поиск" + placeholder = "Введите что-нибудь..." + resultTitle = "Найдено #PAGES_COUNT страниц (за #TIME_SECONDS с.)" + +[footer] + builtWith = "Создано при помощи {{ .Generator }}" + designedBy = "Тема {{ .Theme }}, дизайн {{ .DesignedBy }}" + +[cookies] + title = "Согласие на использование файлов cookie" + text = "Мы используем файлы cookie для улучшения вашего опыта просмотра и анализа трафика сайта." + acceptAll = "Принять все" + deny = "Отклонить" + managePreferences = "Управление настройками" + settingsTitle = "Настройки файлов cookie" + savePreferences = "Сохранить настройки" + cancel = "Отмена" + footerLink = "Настройки файлов cookie" + commentsDisabled = "Комментарии отключены, пока вы не примете функциональные файлы cookie." + + [cookies.necessary] + title = "Необходимые" + text = "Эти файлы cookie необходимы для работы сайта и не могут быть отключены." + + [cookies.analytics] + title = "Аналитика" + text = "Эти файлы cookie помогают нам понять, как посетители взаимодействуют с нашим сайтом." + + [cookies.functional] + title = "Функциональные" + text = "Эти файлы cookie включают такие функции, как комментарии и встроенный контент." diff --git a/themes/stack/i18n/sk.toml b/themes/stack/i18n/sk.toml new file mode 100644 index 0000000..0a65fac --- /dev/null +++ b/themes/stack/i18n/sk.toml @@ -0,0 +1,82 @@ +toggleMenu = "Skryť menu" +darkMode = "Tmavý režim" + +[list] + section = "Kategórie" + + [list.page] + one = "{{ .Count }} stránka" + other = "{{ .Count }} stránok" + + [list.subsection] + one = "Podkategória" + other = "Podkategórie" + +[article] + back = "Späť" + tableOfContents = "Obsah" + relatedContent = "Súvisiace" + lastUpdatedOn = "Naposledy aktualizované" + + [article.readingTime] + one = "{{ .Count }} minúta" + other = "{{ .Count }} minút" + + [article.alert] + note = "Poznámka" + tip = "Tip" + important = "Dôležité" + warning = "Varovanie" + caution = "Upozornenie" + + [article.codeblock] + copy = "Kopírovať" + copied = "Skopírované!" + +[notFound] + title = "Nenájdené" + subtitle = "Tato stránka neexistuje" + +[widget] + [widget.archives] + title = "Archívy" + more = "Viac" + + [widget.tagCloud] + title = "Štítky" + + [widget.categoriesCloud] + title = "Kategórie" + +[search] + title = "Hladať" + placeholder = "Zadajte niečo..." + resultTitle = "#PAGES_COUNT stránok (#TIME_SECONDS sekúnd)" + +[footer] + builtWith = "Vytvorené pomocou {{ .Generator }}" + designedBy = "Šablóna {{ .Theme }} od {{ .DesignedBy }}" + +[cookies] + title = "Súhlas s cookies" + text = "Používame cookies na zlepšenie vášho prehliadania a analýzu návštevnosti webu." + acceptAll = "Prijať všetko" + deny = "Odmietnuť" + managePreferences = "Spravovať predvoľby" + settingsTitle = "Predvoľby cookies" + savePreferences = "Uložiť predvoľby" + cancel = "Zrušiť" + footerLink = "Nastavenia cookies" + commentsDisabled = "Komentáre sú zakázané, kým neprijmete funkčné cookies." + + [cookies.necessary] + title = "Nutné" + text = "Tieto cookies sú nutné pre fungovanie webu a nemožno ich zakázať." + + [cookies.analytics] + title = "Analytické" + text = "Tieto cookies nám pomáhajú pochopiť, ako návštevníci interagujú s naším webom." + + [cookies.functional] + title = "Funkčné" + text = "Tieto cookies umožňujú funkcie ako komentáre a vložený obsah." diff --git a/themes/stack/i18n/th.toml b/themes/stack/i18n/th.toml new file mode 100644 index 0000000..2c96e43 --- /dev/null +++ b/themes/stack/i18n/th.toml @@ -0,0 +1,79 @@ +toggleMenu = "สลับเมนู" +darkMode = "ธีมมืด" + +[list] + section = "หมวดหมู่" + + [list.page] + one = "{{ .Count }} หน้า" + other = "{{ .Count }} หน้า" + + [list.subsection] + one = "หมวดหมู่ย่อย" + other = "หมวดหมู่ย่อยอื่นๆ" + +[article] + back = "กลับไป" + tableOfContents = "สารบัญ" + relatedContent = "เนื้อหาคล้ายคลึงกัน" + lastUpdatedOn = "อัปเดตล่าสุดเมื่อ" + + [article.readingTime] + one = "น่าจะใช้เวลา {{ .Count }} นาทีในการอ่าน" + other = "น่าจะใช้เวลา {{ .Count }} นาทีในการอ่าน" + + [article.alert] + note = "หมายเหตุ" + tip = "เคล็ดลับ" + important = "สำคัญ" + warning = "คำเตือน" + caution = "ระวัง" + + [article.codeblock] + copy = "คัดลอก" + copied = "คัดลอกแล้ว!" + +[notFound] + title = "ไม่พบหัวข้อ" + subtitle = "ไม่พบหน้านี้ในระบบ" + +[widget] + [widget.archives] + title = "เนื้อหาที่เก็บถาวรแล้ว" + more = "อื่นๆ นอกจากนี้" + + [widget.tagCloud] + title = "แท็ก" + +[search] + title = "ค้นหา" + placeholder = "พิมพ์เพื่อค้นหา ..." + resultTitle = "#PAGES_COUNT pages (#TIME_SECONDS seconds)" + +[footer] + builtWith = "ถูกสร้างด้วย {{ .Generator }}" + designedBy = "ธีม {{ .Theme }} ออกแบบโดย {{ .DesignedBy }}" + +[cookies] + title = "ความยินยอมคุกกี้" + text = "เราใช้คุกกี้เพื่อปรับปรุงประสบการณ์การเรียกดูของคุณและวิเคราะห์การเข้าชมเว็บไซต์" + acceptAll = "ยอมรับทั้งหมด" + deny = "ปฏิเสธ" + managePreferences = "จัดการการตั้งค่า" + settingsTitle = "การตั้งค่าคุกกี้" + savePreferences = "บันทึกการตั้งค่า" + cancel = "ยกเลิก" + footerLink = "การตั้งค่าคุกกี้" + commentsDisabled = "ความคิดเห็นถูกปิดการใช้งานจนกว่าคุณจะยอมรับคุกกี้เชิงหน้าที่" + + [cookies.necessary] + title = "จำเป็น" + text = "คุกกี้เหล่านี้จำเป็นสำหรับการทำงานของเว็บไซต์และไม่สามารถปิดการใช้งานได้" + + [cookies.analytics] + title = "การวิเคราะห์" + text = "คุกกี้เหล่านี้ช่วยให้เราเข้าใจว่าผู้เยี่ยมชมโต้ตอบกับเว็บไซต์ของเราอย่างไร" + + [cookies.functional] + title = "เชิงหน้าที่" + text = "คุกกี้เหล่านี้เปิดใช้งานคุณสมบัติต่างๆ เช่น ความคิดเห็นและเนื้อหาที่ฝังไว้" diff --git a/themes/stack/i18n/tr.toml b/themes/stack/i18n/tr.toml new file mode 100644 index 0000000..76cea58 --- /dev/null +++ b/themes/stack/i18n/tr.toml @@ -0,0 +1,82 @@ +toggleMenu = "Menüyü Gizle" +darkMode = "Koyu Mod" + +[list] + section = "Bölüm" + + [list.page] + one = "{{ .Count }} makale" + other = "{{ .Count }} makale" + + [list.subsection] + one = "Alt bölüm" + other = "Alt bölümler" + +[article] + back = "Geri" + tableOfContents = "İçindekiler" + relatedContent = "Benzer içerikler" + lastUpdatedOn = "Son güncelleme" + + [article.readingTime] + one = "{{ .Count }} dakika okuma süresi" + other = "{{ .Count }} dakika okuma süresi" + + [article.alert] + note = "Not" + tip = "İpucu" + important = "Önemli" + warning = "Uyarı" + caution = "Dikkat" + + [article.codeblock] + copy = "Kopyala" + copied = "Kopyalandı!" + +[notFound] + title = "Bulunamadı" + subtitle = "Aradığınız sayfa mevcut değil." + +[widget] + [widget.archives] + title = "Arşiv" + more = "Daha fazla" + + [widget.tagCloud] + title = "Etiketler" + + [widget.categoriesCloud] + title = "Kategoriler" + +[search] + title = "Arama" + placeholder = "Birşeyler yazın..." + resultTitle = "#PAGES_COUNT sayfa (#TIME_SECONDS saniye)" + +[footer] + builtWith = "{{ .Generator }} ile oluşturuldu." + designedBy = "{{ .Theme }} teması {{ .DesignedBy }} tarafından tasarlandı" + +[cookies] + title = "Çerez Onayı" + text = "Tarayıcı deneyiminizi iyileştirmek ve site trafiğini analiz etmek için çerezler kullanıyoruz." + acceptAll = "Tümünü Kabul Et" + deny = "Reddet" + managePreferences = "Tercihleri yönet" + settingsTitle = "Çerez Tercihleri" + savePreferences = "Tercihleri kaydet" + cancel = "İptal" + footerLink = "Çerez ayarları" + commentsDisabled = "İşlevsel çerezleri kabul edene kadar yorumlar devre dışıdır." + + [cookies.necessary] + title = "Gerekli" + text = "Bu çerezler web sitesinin çalışması için gereklidir ve devre dışı bırakılamaz." + + [cookies.analytics] + title = "Analitik" + text = "Bu çerezler, ziyaretçilerin web sitemizle nasıl etkileşimde bulunduğunu anlamamıza yardımcı olur." + + [cookies.functional] + title = "İşlevsel" + text = "Bu çerezler, yorumlar ve gömülü içerik gibi özellikleri etkinleştirir." diff --git a/themes/stack/i18n/uk.toml b/themes/stack/i18n/uk.toml new file mode 100644 index 0000000..6053cb0 --- /dev/null +++ b/themes/stack/i18n/uk.toml @@ -0,0 +1,83 @@ +toggleMenu = "Показати меню" +darkMode = "Темна тема" + +[list] + section = "Секція" + + [list.page] + one = "{{ .Count }} сторінка" + few = "{{ .Count }} сторінки" + other = "{{ .Count }} сторінок" + + [list.subsection] + one = "Підсекція" + other = "Підсекції" + +[article] + back = "Назад" + tableOfContents = "Зміст" + relatedContent = "Схожі матеріали" + lastUpdatedOn = "Востаннє оновлено" + + [article.readingTime] + one = "Час читання: {{ .Count }} хв" + other = "Час читання: {{ .Count }} хв" + + [article.alert] + note = "Примітка" + tip = "Порада" + important = "Важливо" + warning = "Попередження" + caution = "Обережно" + + [article.codeblock] + copy = "Копіювати" + copied = "Скопійовано!" + +[notFound] + title = "Не знайдено" + subtitle = "Ця сторінка не існує" + +[widget] + [widget.archives] + title = "Архіви" + more = "Більше" + + [widget.tagCloud] + title = "Теґи" + + [widget.categoriesCloud] + title = "Категорії" + +[search] + title = "Пошук" + placeholder = "Напишіть що-небудь..." + resultTitle = "#PAGES_COUNT сторінок (#TIME_SECONDS секунд)" + +[footer] + builtWith = "Створено з {{ .Generator }}" + designedBy = "Тема {{ .Theme }}, дизайн {{ .DesignedBy }}" + +[cookies] + title = "Згода на використання файлів cookie" + text = "Ми використовуємо файли cookie для покращення вашого досвіду перегляду та аналізу трафіку сайту." + acceptAll = "Прийняти все" + deny = "Відхилити" + managePreferences = "Керувати налаштуваннями" + settingsTitle = "Налаштування файлів cookie" + savePreferences = "Зберегти налаштування" + cancel = "Скасувати" + footerLink = "Налаштування файлів cookie" + commentsDisabled = "Коментарі вимкнено, доки ви не приймете функціональні файли cookie." + + [cookies.necessary] + title = "Необхідні" + text = "Ці файли cookie необхідні для роботи сайту і не можуть бути вимкнені." + + [cookies.analytics] + title = "Аналітика" + text = "Ці файли cookie допомагають нам зрозуміти, як відвідувачі взаємодіють з нашим сайтом." + + [cookies.functional] + title = "Функціональні" + text = "Ці файли cookie вмикають такі функції, як коментарі та вбудований контент." diff --git a/themes/stack/i18n/vi.toml b/themes/stack/i18n/vi.toml new file mode 100644 index 0000000..516c987 --- /dev/null +++ b/themes/stack/i18n/vi.toml @@ -0,0 +1,82 @@ +toggleMenu = "Hiển thị Menu" +darkMode = "Chế độ nền tối" + +[list] + section = "Section" + + [list.page] + one = "{{ .Count }} trang" + other = "{{ .Count }} trang" + + [list.subsection] + one = "Subsection" + other = "Subsections" + +[article] + back = "Trở lại" + tableOfContents = "Mục lục" + relatedContent = "Bài viết liên quan" + lastUpdatedOn = "Cập nhật lần cuối" + + [article.readingTime] + one = "{{ .Count }} phút đọc" + other = "{{ .Count }} phút đọc" + + [article.alert] + note = "Ghi chú" + tip = "Mẹo" + important = "Quan trọng" + warning = "Cảnh báo" + caution = "Thận trọng" + + [article.codeblock] + copy = "Sao chép" + copied = "Đã sao chép!" + +[notFound] + title = "Không tìm thấy" + subtitle = "Trang này không tồn tại" + +[widget] + [widget.archives] + title = "Lưu trữ" + more = "Còn nữa" + + [widget.tagCloud] + title = "Nhãn dán" + + [widget.categoriesCloud] + title = "Chuỗi bài" + +[search] + title = "Tìm kiếm" + placeholder = "Nhập từ khoá ..." + resultTitle = "#PAGES_COUNT trang (#TIME_SECONDS giây)" + +[footer] + builtWith = "Built with {{ .Generator }}" + designedBy = "Theme {{ .Theme }} thiết kế bởi {{ .DesignedBy }}" + +[cookies] + title = "Đồng ý cookie" + text = "Chúng tôi sử dụng cookie để nâng cao trải nghiệm duyệt web của bạn và phân tích lưu lượng truy cập trang web." + acceptAll = "Chấp nhận tất cả" + deny = "Từ chối" + managePreferences = "Quản lý tùy chọn" + settingsTitle = "Tùy chọn cookie" + savePreferences = "Lưu tùy chọn" + cancel = "Hủy" + footerLink = "Cài đặt cookie" + commentsDisabled = "Bình luận bị vô hiệu hóa cho đến khi bạn chấp nhận cookie chức năng." + + [cookies.necessary] + title = "Cần thiết" + text = "Những cookie này là cần thiết để trang web hoạt động và không thể vô hiệu hóa." + + [cookies.analytics] + title = "Phân tích" + text = "Những cookie này giúp chúng tôi hiểu cách khách truy cập tương tác với trang web của chúng tôi." + + [cookies.functional] + title = "Chức năng" + text = "Những cookie này cho phép các tính năng như bình luận và nội dung nhúng." diff --git a/themes/stack/i18n/zh-hant-hk.toml b/themes/stack/i18n/zh-hant-hk.toml new file mode 100644 index 0000000..59e3052 --- /dev/null +++ b/themes/stack/i18n/zh-hant-hk.toml @@ -0,0 +1,78 @@ +toggleMenu = "切換選單" +darkMode = "深色模式" + +[list] + page = "{{ .Count }} 篇文章" + section = "章節" + subsection = "子章節" + +[article] + back = "返回" + tableOfContents = "目錄" + relatedContent = "相關內容" + lastUpdatedOn = "上次改過於" + readingTime = "閱讀時間 {{ .Count }} 分鐘" + + [article.alert] + note = "備註" + tip = "提示" + important = "重要" + warning = "警告" + caution = "注意" + + [article.codeblock] + copy = "複製" + copied = "已複製!" + +[notFound] + title = "找不到頁面" + subtitle = "本頁面並不存在" + +[widget] + [widget.archives] + title = "歸檔" + more = "更多" + + [widget.tagCloud] + title = "標籤" + + [widget.categoriesCloud] + title = "分類" + +[search] + title = "搜尋" + placeholder = "輸入關鍵字..." + resultTitle = "找到 #PAGES_COUNT 個結果 (用時 #TIME_SECONDS 秒)" + +[footer] + builtWith = "使用 {{ .Generator }} 建立" + designedBy = "主題 {{ .Theme }} 由 {{ .DesignedBy }} 設計" + +[cookies] + title = "Cookie 同意" + text = "我們使用 Cookie 來增強您的瀏覽體驗並分析網站流量。" + acceptAll = "接受全部" + deny = "拒絕" + managePreferences = "管理偏好設定" + settingsTitle = "Cookie 偏好設定" + savePreferences = "儲存偏好設定" + cancel = "取消" + footerLink = "Cookie 設定" + commentsDisabled = "評論已停用,直到您接受功能性 Cookie。" + + [cookies.necessary] + title = "必要" + text = "這些 Cookie 是網站運作所必需的,無法停用。" + + [cookies.analytics] + title = "分析" + text = "這些 Cookie 幫助我們了解訪客如何與我們的網站互動。" + + [cookies.functional] + title = "功能性" + text = "這些 Cookie 啟用評論和嵌入內容等功能." + +[pagination] + jumpToPage = "跳轉到頁碼" + jump = "跳轉" + pressEnter = "按 Enter 鍵跳轉" diff --git a/themes/stack/i18n/zh-hant-tw.toml b/themes/stack/i18n/zh-hant-tw.toml new file mode 100644 index 0000000..83bf589 --- /dev/null +++ b/themes/stack/i18n/zh-hant-tw.toml @@ -0,0 +1,78 @@ +toggleMenu = "切換選單" +darkMode = "夜晚模式" + +[list] + section = "段落" + page = "第 {{ .Count }} 頁" + subsection = "小節" + +[article] + back = "返回" + tableOfContents = "目錄" + relatedContent = "相關文章" + lastUpdatedOn = "最後更新" + readingTime = "閱讀時間: {{ .Count }} 分鐘" + + [article.alert] + note = "備註" + tip = "提示" + important = "重要" + warning = "警告" + caution = "注意" + + [article.codeblock] + copy = "複製" + copied = "已複製!" + +[notFound] + title = "404 錯誤" + subtitle = "頁面不存在" + +[widget] + [widget.archives] + title = "紀錄" + more = "更多" + + [widget.tagCloud] + title = "標籤雲" + + [widget.categoriesCloud] + title = "分類" + +[search] + title = "搜尋" + placeholder = "輸入關鍵字..." + resultTitle = "#PAGES_COUNT 個結果 (用時 #TIME_SECONDS 秒)" + +[footer] + builtWith = "使用 {{ .Generator }} 建立" + designedBy = "主題 {{ .Theme }} 由 {{ .DesignedBy }} 設計" + +[cookies] + title = "Cookie 同意" + text = "我們使用 Cookie 來增強您的瀏覽體驗並分析網站流量。" + acceptAll = "接受全部" + deny = "拒絕" + managePreferences = "管理偏好設定" + settingsTitle = "Cookie 偏好設定" + savePreferences = "儲存偏好設定" + cancel = "取消" + footerLink = "Cookie 設定" + commentsDisabled = "評論已停用,直到您接受功能性 Cookie。" + + [cookies.necessary] + title = "必要" + text = "這些 Cookie 是網站運作所必需的,無法停用。" + + [cookies.analytics] + title = "分析" + text = "這些 Cookie 幫助我們了解訪客如何與我們的網站互動。" + + [cookies.functional] + title = "功能性" + text = "這些 Cookie 啟用評論和嵌入內容等功能." + +[pagination] + jumpToPage = "跳轉到頁碼" + jump = "跳轉" + pressEnter = "按 Enter 鍵跳轉" diff --git a/themes/stack/i18n/zh.toml b/themes/stack/i18n/zh.toml new file mode 100644 index 0000000..79d3678 --- /dev/null +++ b/themes/stack/i18n/zh.toml @@ -0,0 +1,78 @@ +toggleMenu = "切换菜单" +darkMode = "暗色模式" + +[list] + page = "{{ .Count }} 个页面" + section = "章节" + subsection = "子章节" + +[article] + back = "返回" + tableOfContents = "目录" + relatedContent = "相关文章" + lastUpdatedOn = "最后更新于" + readingTime = "阅读时长: {{ .Count }} 分钟" + + [article.alert] + note = "备注" + tip = "提示" + important = "重要" + warning = "警告" + caution = "注意" + + [article.codeblock] + copy = "复制" + copied = "已复制!" + +[notFound] + title = "404 错误" + subtitle = "页面不存在" + +[widget] + [widget.archives] + title = "归档" + more = "更多" + + [widget.tagCloud] + title = "标签云" + + [widget.categoriesCloud] + title = "分类" + +[search] + title = "搜索" + placeholder = "输入关键词..." + resultTitle = "#PAGES_COUNT 个结果 (用时 #TIME_SECONDS 秒)" + +[footer] + builtWith = "使用 {{ .Generator }} 构建" + designedBy = "主题 {{ .Theme }} 由 {{ .DesignedBy }} 设计" + +[cookies] + title = "Cookie 同意" + text = "我们使用 Cookie 来增强您的浏览体验并分析网站流量。" + acceptAll = "接受所有" + deny = "拒绝" + managePreferences = "管理偏好设置" + settingsTitle = "Cookie 偏好设置" + savePreferences = "保存偏好设置" + cancel = "取消" + footerLink = "Cookie 设置" + commentsDisabled = "评论已禁用,直到您接受功能性 Cookie。" + + [cookies.necessary] + title = "必要" + text = "这些 Cookie 是网站运行所必需的,无法禁用。" + + [cookies.analytics] + title = "分析" + text = "这些 Cookie 帮助我们了解访问者如何与我们的网站互动。" + + [cookies.functional] + title = "功能性" + text = "这些 Cookie 启用评论和嵌入内容等功能." + +[pagination] + jumpToPage = "跳转到页码" + jump = "跳转" + pressEnter = "按 Enter 键跳转" diff --git a/themes/stack/images/screenshot.png b/themes/stack/images/screenshot.png new file mode 100644 index 0000000..efcfd8a Binary files /dev/null and b/themes/stack/images/screenshot.png differ diff --git a/themes/stack/images/tn.png b/themes/stack/images/tn.png new file mode 100644 index 0000000..f85fd75 Binary files /dev/null and b/themes/stack/images/tn.png differ diff --git a/themes/stack/layouts/404.html b/themes/stack/layouts/404.html new file mode 100644 index 0000000..8ebfba7 --- /dev/null +++ b/themes/stack/layouts/404.html @@ -0,0 +1,45 @@ +{{ define "main" }} +
+

{{ T "notFound.title" }}

+

{{ T "notFound.subtitle" }}

+
+ + {{- $query := first 1 (where .Site.Pages "Layout" "==" "search") -}} + {{- $searchPage := index $query 0 -}} + + {{- with $searchPage -}} +
+

+ + + + +

+
+ +
+

+
+
+ + + + {{- $opts := dict "minify" hugo.IsProduction "JSXFactory" "createElement" -}} + {{- $searchScript := resources.Get "ts/search.tsx" | js.Build $opts -}} + + + + {{- end -}} + {{ partialCached "footer/footer" . }} +{{ end }} diff --git a/themes/stack/layouts/_default/list.html b/themes/stack/layouts/_default/list.html new file mode 100644 index 0000000..777bb41 --- /dev/null +++ b/themes/stack/layouts/_default/list.html @@ -0,0 +1,78 @@ +{{ define "main" }} +
+

+ {{ if eq .Parent (.GetPage "/") }} + {{ T "list.section" }} + {{ else }} + {{ .Parent.Title }} + {{ end }} +

+ +
+
+

{{ T "list.page" (len .Pages) }}

+

{{ .Title }}

+ {{ with .Params.description }} +

{{ . }}

+ {{ end }} +
+ + {{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}} + {{ if $image }} +
+ {{ partial "helper/thumbnail-image" (dict + "Resource" $image.Resource + "Width" 60 + "Height" 60 + "Resize" .Site.Params.ImageProcessing.Thumbnail.Enabled + "Attributes" (dict + "src" $image.Permalink + "alt" .Title + "loading" "lazy" + "width" $image.Width + "height" $image.Height + ) + ) }} +
+ {{ end }} +
+
+ + {{- $pages := partial "helper/pages.html" . -}} + {{- $subsections := .Sections -}} + + {{- if and (eq (len $pages) (len $subsections)) (eq $pages $subsections) -}} + {{- $subsections = slice -}} + {{- end -}} + + {{- with $subsections -}} + + {{- end -}} + + {{/* List only pages that are not a subsection */}} + {{ $paginator := partial "helper/paginator.html" . }} + {{ with $paginator }} +
+ {{ range .Pages }} + {{ partial "article-list/compact" . }} + {{ end }} +
+ {{ end }} + + {{- partial "pagination.html" . -}} + + {{ partialCached "footer/footer" . }} +{{ end }} + +{{ define "right-sidebar" }} + {{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }} +{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/_default/single.html b/themes/stack/layouts/_default/single.html new file mode 100644 index 0000000..5965572 --- /dev/null +++ b/themes/stack/layouts/_default/single.html @@ -0,0 +1,46 @@ +{{ define "body-class" }} + article-page + {{/* + Enable the right sidebar if + - Widget different from 'TOC' is enabled + - TOC is enabled and not empty + */}} + {{- $HasWidgetNotTOC := false -}} + {{- $TOCWidgetEnabled := false -}} + {{- range .Site.Params.widgets.page -}} + {{- if ne .type "toc" -}} + {{ $HasWidgetNotTOC = true -}} + {{- else -}} + {{ $TOCWidgetEnabled = true -}} + {{- end -}} + {{- end -}} + + {{- $TOCEnabled := and (default .Site.Params.article.toc .Params.toc) $TOCWidgetEnabled -}} + {{- $hasTOC := ge (len .TableOfContents) 100 -}} + {{- .Scratch.Set "TOCEnabled" (and $TOCEnabled $hasTOC) -}} + + {{- .Scratch.Set "hasWidget" (or $HasWidgetNotTOC (and $TOCEnabled $hasTOC)) -}} +{{ end }} + +{{ define "main" }} + {{ partial "article/article.html" . }} + + {{ if .Params.links }} + {{ partial "article/components/links" . }} + {{ end }} + + {{ partial "article/components/related-content" . }} + + {{ if not (eq .Params.comments false) }} + {{ partial "comments/include" . }} + {{ end }} + + {{ partialCached "footer/footer" . }} + + {{ partialCached "article/components/photoswipe" . }} + {{ partial "article/components/mermaid" . }} +{{ end }} + +{{ define "right-sidebar" }} + {{ if .Scratch.Get "hasWidget" }}{{ partial "sidebar/right.html" (dict "Context" . "Scope" "page") }}{{ end}} +{{ end }} diff --git a/themes/stack/layouts/_markup/render-blockquote.html b/themes/stack/layouts/_markup/render-blockquote.html new file mode 100644 index 0000000..032afbd --- /dev/null +++ b/themes/stack/layouts/_markup/render-blockquote.html @@ -0,0 +1,26 @@ +{{ if eq .Type "alert" }} + {{- $type := .AlertType -}} + {{- $icons := site.Params.article.alertIcon -}} + {{- $icon := index $icons $type -}} +
+
+ + {{- $icon -}} + + + {{- with .AlertTitle -}} + {{ . }} + {{- else -}} + {{ or (i18n (printf "article.alert.%s" $type)) (title $type) }} + {{- end -}} + +
+
+ {{ .Text }} +
+
+{{ else }} +
+ {{ .Text }} +
+{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/_markup/render-codeblock-mermaid.html b/themes/stack/layouts/_markup/render-codeblock-mermaid.html new file mode 100644 index 0000000..bb0f689 --- /dev/null +++ b/themes/stack/layouts/_markup/render-codeblock-mermaid.html @@ -0,0 +1,4 @@ + +{{- .Page.Store.Set "hasMermaid" true -}} diff --git a/themes/stack/layouts/_markup/render-heading.html b/themes/stack/layouts/_markup/render-heading.html new file mode 100644 index 0000000..f79308a --- /dev/null +++ b/themes/stack/layouts/_markup/render-heading.html @@ -0,0 +1,6 @@ + + {{- if site.Params.Article.HeadingAnchor -}} + + {{- end -}} + {{ .Text | safeHTML }} + \ No newline at end of file diff --git a/themes/stack/layouts/_markup/render-image.html b/themes/stack/layouts/_markup/render-image.html new file mode 100644 index 0000000..8e8871b --- /dev/null +++ b/themes/stack/layouts/_markup/render-image.html @@ -0,0 +1,30 @@ +{{- $image := partial "helper/image" (dict "Resources" .Page.Resources "Image" .Destination "Context" .Page) -}} +{{- $alt := .PlainText | safeHTML -}} +{{- $title := .Title | markdownify -}} + +{{/* There are some types of images that don't have width and height attributes, such as SVG */}} +{{- $galleryImage := and $image.Height $image.Width -}} + +{{- $attributes := dict -}} +{{- if $galleryImage -}} + {{- $attributes = (dict + "data-flex-grow" (div (mul $image.Width 100) $image.Height) + "data-flex-basis" (printf "%dpx" (div (mul $image.Width 240) $image.Height)) + ) -}} +{{- end -}} + +{{ partial "helper/responsive-image" (dict + "Resource" $image.Resource + "Widths" (cond .Page.Site.Params.ImageProcessing.Content.Enabled .Page.Site.Params.ImageProcessing.Content.Widths nil) + "Attributes" (merge $attributes (dict + "src" $image.Permalink + "width" $image.Width + "height" $image.Height + "alt" $alt + "title" $title + "data-title-escaped" ($title | htmlEscape) + "class" (cond $galleryImage "gallery-image" "") + "loading" "lazy" + "sizes" "(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" + )) +) }} \ No newline at end of file diff --git a/themes/stack/layouts/_markup/render-link.html b/themes/stack/layouts/_markup/render-link.html new file mode 100644 index 0000000..843854d --- /dev/null +++ b/themes/stack/layouts/_markup/render-link.html @@ -0,0 +1,3 @@ +{{ .Text | safeHTML }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/article-list/compact.html b/themes/stack/layouts/_partials/article-list/compact.html new file mode 100644 index 0000000..ec01bfd --- /dev/null +++ b/themes/stack/layouts/_partials/article-list/compact.html @@ -0,0 +1,49 @@ +
+ +
+

+ {{- .Title -}} +

+ + {{- $description := .Params.description -}} + {{- $IsTerm := in .Kind "term" -}} + {{- if $IsTerm -}} + {{- $description = $description | default (T "list.page" (len .Pages)) -}} + {{- end -}} + {{- if or $description (not .Date.IsZero) -}} + {{/* + If it's a term, the date is extracted from the first page of the collection, which is not very useful. + In that case, we prefer to display the description (if set), or the number of pages within the collection. + */}} +
+ {{- if or $IsTerm .Date.IsZero -}} + {{- $description -}} + {{- else if not .Date.IsZero -}} + + {{- end }} +
+ {{- end -}} +
+ + {{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}} + {{ if $image }} +
+ {{ partial "helper/thumbnail-image" (dict + "Resource" $image.Resource + "Width" 60 + "Height" 60 + "Resize" .Site.Params.ImageProcessing.Thumbnail.Enabled + "Attributes" (dict + "src" $image.Permalink + "alt" .Title + "loading" "lazy" + "width" $image.Width + "height" $image.Height + ) + ) }} +
+ {{ end }} +
+
\ No newline at end of file diff --git a/themes/stack/layouts/_partials/article-list/default.html b/themes/stack/layouts/_partials/article-list/default.html new file mode 100644 index 0000000..0a1a4b0 --- /dev/null +++ b/themes/stack/layouts/_partials/article-list/default.html @@ -0,0 +1,4 @@ +{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }} +
+ {{ partial "article/components/header" (dict "Page" . "IsList" true) }} +
\ No newline at end of file diff --git a/themes/stack/layouts/_partials/article-list/tile.html b/themes/stack/layouts/_partials/article-list/tile.html new file mode 100644 index 0000000..8e9df0f --- /dev/null +++ b/themes/stack/layouts/_partials/article-list/tile.html @@ -0,0 +1,37 @@ +{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }} + \ No newline at end of file diff --git a/themes/stack/layouts/_partials/article/article.html b/themes/stack/layouts/_partials/article/article.html new file mode 100644 index 0000000..88d4f53 --- /dev/null +++ b/themes/stack/layouts/_partials/article/article.html @@ -0,0 +1,27 @@ +
+ {{ partial "article/components/header" (dict "Page" . "IsList" false) }} + + {{ if .Scratch.Get "TOCEnabled" }} + + {{ end }} + + {{ partial "article/components/content" . }} + + {{ partial "article/components/footer" . }} + + {{ if or .Params.math .Site.Params.article.math }} + {{ partialCached "article/components/math.html" . }} + {{ end }} +
\ No newline at end of file diff --git a/themes/stack/layouts/_partials/article/components/content.html b/themes/stack/layouts/_partials/article/components/content.html new file mode 100644 index 0000000..61e536c --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/content.html @@ -0,0 +1,5 @@ +
+ + {{ $wrappedTable := printf "
${1}
" }} + {{ .Content | replaceRE "((?:.|\n)+?
)" $wrappedTable | safeHTML }} +
diff --git a/themes/stack/layouts/_partials/article/components/details.html b/themes/stack/layouts/_partials/article/components/details.html new file mode 100644 index 0000000..b7c6c14 --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/details.html @@ -0,0 +1,76 @@ +{{- $IsList := .IsList -}} +{{- $Page := .Page -}} +
+ {{ if $Page.Params.categories }} + + {{ end }} + +
+

+ + {{- $Page.Title -}} + +

+ + {{ with $Page.Params.description }} +

+ {{ . }} +

+ {{ end }} +
+ + {{ $showReadingTime := $Page.Params.readingTime | default ($Page.Site.Params.article.readingTime) }} + {{ $showDate := not $Page.Date.IsZero }} + {{ $showTime := or $showDate $showReadingTime }} + {{ $showTranslations := $Page.IsTranslated }} + {{ $showTags := and $Page.Site.Params.Article.List.ShowTags ($Page.GetTerms "tags") $IsList }} + + {{ if or $showTime $showTranslations $showTags }} +
+ {{ if $showTime }} +
+ {{ if $showDate }} + {{ partial "helper/icon" "date" }} + + {{ end }} + + {{ if $showReadingTime }} + {{ partial "helper/icon" "clock" }} + + {{ end }} +
+ {{ end }} + + {{ if $showTranslations }} +
+ {{ partial "helper/icon" "language" }} + {{ range $Page.Translations }} + {{ .Lang }} + {{ end }} +
+ {{ end }} + + {{ if $showTags }} + + {{ end }} +
+ {{ end }} +
diff --git a/themes/stack/layouts/_partials/article/components/footer.html b/themes/stack/layouts/_partials/article/components/footer.html new file mode 100644 index 0000000..7fecbac --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/footer.html @@ -0,0 +1,19 @@ +
+ {{ partial "article/components/tags" . }} + + {{ if and (.Site.Params.article.license.enabled) (not (eq .Params.license false)) }} +
+ {{ partial "helper/icon" "copyright" }} + {{ default .Site.Params.article.license.default .Params.license | markdownify }} +
+ {{ end }} + + {{- if ne .Lastmod .Date -}} +
+ {{ partial "helper/icon" "clock" }} + + {{ T "article.lastUpdatedOn" }} {{ .Lastmod | time.Format .Site.Params.dateFormat.lastUpdated }} + +
+ {{- end -}} +
diff --git a/themes/stack/layouts/_partials/article/components/header.html b/themes/stack/layouts/_partials/article/components/header.html new file mode 100644 index 0000000..0d65fc5 --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/header.html @@ -0,0 +1,25 @@ +{{- $IsList := .IsList -}} +{{- $Page := .Page -}} +
+ {{- $image := partial "helper/image" (dict "Image" $Page.Params.image "Resources" $Page.Resources "Context" $Page) -}} + {{ if $image }} +
+ + {{ partial "helper/responsive-image" (dict + "Resource" $image.Resource + "Widths" (cond $Page.Site.Params.ImageProcessing.Content.Enabled $Page.Site.Params.ImageProcessing.Content.Widths nil) + "Attributes" (dict + "src" $image.Permalink + "width" $image.Width + "height" $image.Height + "alt" (printf "Featured image of post %s" $Page.Title) + "loading" "lazy" + "sizes" "(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" + ) + ) }} + +
+ {{ end }} + + {{ partial "article/components/details" (dict "Page" $Page "IsList" $IsList) }} +
\ No newline at end of file diff --git a/themes/stack/layouts/_partials/article/components/links.html b/themes/stack/layouts/_partials/article/components/links.html new file mode 100644 index 0000000..a6fa768 --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/links.html @@ -0,0 +1,39 @@ +
+ {{ range $link := .Params.links }} +
+ +
+

+ {{- $link.title -}} +

+
+ {{ with $link.description }} + {{ . }} + {{ else }} + {{ $link.website }} + {{ end }} +
+
+ + {{- $image := partial "helper/image" (dict "Image" $link.image "Resources" $.Resources "Context" .) -}} + {{- with $image -}} +
+ {{ partial "helper/thumbnail-image" (dict + "Resource" .Resource + "Width" 60 + "Height" 60 + "Resize" $.Site.Params.ImageProcessing.Thumbnail.Enabled + "Attributes" (dict + "src" $image.Permalink + "alt" $link.title + "loading" "lazy" + "width" .Width + "height" .Height + ) + ) }} +
+ {{ end }} +
+
+ {{ end }} +
\ No newline at end of file diff --git a/themes/stack/layouts/_partials/article/components/math.html b/themes/stack/layouts/_partials/article/components/math.html new file mode 100644 index 0000000..a7ce654 --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/math.html @@ -0,0 +1,21 @@ +{{- partial "helper/external" (dict "Context" . "Namespace" "KaTeX") -}} + \ No newline at end of file diff --git a/themes/stack/layouts/_partials/article/components/mermaid.html b/themes/stack/layouts/_partials/article/components/mermaid.html new file mode 100644 index 0000000..dd75faf --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/mermaid.html @@ -0,0 +1,42 @@ +{{- if .Store.Get "hasMermaid" -}} +{{- $cfg := site.Params.article.mermaid | default dict -}} + +
+
+
+ + + + +
+ +
+
+
+
+
+ + +{{- $opts := dict "minify" hugo.IsProduction "format" "esm" -}} +{{- $script := resources.Get "ts/mermaid.ts" | js.Build $opts -}} +{{- $jsConfig := dict + "transparentBackground" ($cfg.transparentBackground | default false) + "lightTheme" ($cfg.lightTheme | default "default") + "darkTheme" ($cfg.darkTheme | default "dark") + "lightThemeVariables" ($cfg.lightThemeVariables | default dict) + "darkThemeVariables" ($cfg.darkThemeVariables | default dict) + "securityLevel" ($cfg.securityLevel | default "strict") + "look" ($cfg.look | default "classic") + "htmlLabels" (not (eq ($cfg.htmlLabels | default true) false)) +-}} +{{- with $cfg.maxTextSize }}{{ $jsConfig = merge $jsConfig (dict "maxTextSize" .) }}{{ end -}} +{{- with $cfg.maxEdges }}{{ $jsConfig = merge $jsConfig (dict "maxEdges" .) }}{{ end -}} +{{- with $cfg.fontSize }}{{ $jsConfig = merge $jsConfig (dict "fontSize" .) }}{{ end -}} +{{- with $cfg.fontFamily }}{{ $jsConfig = merge $jsConfig (dict "fontFamily" .) }}{{ end -}} +{{- with $cfg.curve }}{{ $jsConfig = merge $jsConfig (dict "curve" .) }}{{ end -}} +{{- with $cfg.logLevel }}{{ $jsConfig = merge $jsConfig (dict "logLevel" .) }}{{ end -}} + +{{- end -}} diff --git a/themes/stack/layouts/_partials/article/components/photoswipe.html b/themes/stack/layouts/_partials/article/components/photoswipe.html new file mode 100644 index 0000000..1694a9f --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/photoswipe.html @@ -0,0 +1,32 @@ +{{- $opts := dict "minify" hugo.IsProduction "format" "esm" -}} +{{- $galleryScript := resources.Get "ts/gallery.ts" | js.Build $opts -}} + +{{ $style := hugo.Data.external.PhotoSwipe.Style }} +{{ $core := hugo.Data.external.PhotoSwipe.Core }} +{{ $lightbox := hugo.Data.external.PhotoSwipe.Lightbox }} + + diff --git a/themes/stack/layouts/_partials/article/components/related-content.html b/themes/stack/layouts/_partials/article/components/related-content.html new file mode 100644 index 0000000..c121303 --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/related-content.html @@ -0,0 +1,13 @@ +{{ $related := (.Site.RegularPages.Related .) | first 5 }} +{{ with $related }} + +{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/article/components/tags.html b/themes/stack/layouts/_partials/article/components/tags.html new file mode 100644 index 0000000..aae38b4 --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/tags.html @@ -0,0 +1,7 @@ +{{ if .Params.Tags }} + +{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/article/components/toc.html b/themes/stack/layouts/_partials/article/components/toc.html new file mode 100644 index 0000000..e572226 --- /dev/null +++ b/themes/stack/layouts/_partials/article/components/toc.html @@ -0,0 +1,6 @@ +{{/* Remove from .TableOfContents */}} +{{ $toc := .TableOfContents }} +{{ $toc = replace $toc "" "" }} + +{{ return $toc }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/comments/include.html b/themes/stack/layouts/_partials/comments/include.html new file mode 100644 index 0000000..ac329e9 --- /dev/null +++ b/themes/stack/layouts/_partials/comments/include.html @@ -0,0 +1,28 @@ +{{/* + Comments include entry. + - If functional cookie consent is required, render a placeholder + template and load commentsConsent.ts. + - Otherwise, render provider partial directly. +*/}} +{{ if .Site.Params.comments.enabled }} + {{- $needsConsent := and .Site.Params.cookies.enabled .Site.Params.cookies.categories.functional -}} + {{- if $needsConsent -}} + {{/* Consent-gated comments - show placeholder until functional consent */}} + + + + {{- $opts := dict "minify" hugo.IsProduction -}} + {{/* commentsConsent.ts is intentionally built and loaded here as an independent entry */}} + {{- $commentsScript := resources.Get "ts/commentsConsent.ts" | js.Build $opts | fingerprint -}} + + {{- else -}} + {{/* No consent required - load comments normally */}} + {{ partial (printf "comments/provider/%s" .Site.Params.comments.provider) . }} + {{- end -}} +{{ end }} diff --git a/themes/stack/layouts/_partials/comments/provider/artalk.html b/themes/stack/layouts/_partials/comments/provider/artalk.html new file mode 100644 index 0000000..2cfbc8e --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/artalk.html @@ -0,0 +1,30 @@ +{{- $style := resources.Get "scss/partials/comments/artalk.scss" | toCSS | minify -}} +{{- with .Site.Params.comments.artalk -}} + + + +
+ + +{{- end -}} diff --git a/themes/stack/layouts/_partials/comments/provider/beaudar.html b/themes/stack/layouts/_partials/comments/provider/beaudar.html new file mode 100644 index 0000000..761801f --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/beaudar.html @@ -0,0 +1,46 @@ + + + + + diff --git a/themes/stack/layouts/_partials/comments/provider/cactus.html b/themes/stack/layouts/_partials/comments/provider/cactus.html new file mode 100644 index 0000000..ae172d3 --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/cactus.html @@ -0,0 +1,29 @@ +{{- with .Site.Params.comments.cactus -}} +{{- partial "helper/external" (dict "Context" $ "Namespace" "Cactus") -}} + + + +
+ + +{{- end -}} diff --git a/themes/stack/layouts/_partials/comments/provider/comentario.html b/themes/stack/layouts/_partials/comments/provider/comentario.html new file mode 100644 index 0000000..a78323d --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/comentario.html @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/themes/stack/layouts/_partials/comments/provider/cusdis.html b/themes/stack/layouts/_partials/comments/provider/cusdis.html new file mode 100644 index 0000000..d0ed7f1 --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/cusdis.html @@ -0,0 +1,27 @@ +{{- $host := .Site.Params.comments.cusdis.host -}} +
+ + + \ No newline at end of file diff --git a/themes/stack/layouts/_partials/comments/provider/disqus.html b/themes/stack/layouts/_partials/comments/provider/disqus.html new file mode 100644 index 0000000..32e6197 --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/disqus.html @@ -0,0 +1,22 @@ +
+ {{ partial "disqus.html" . }} +
+ + + + \ No newline at end of file diff --git a/themes/stack/layouts/_partials/comments/provider/disqusjs.html b/themes/stack/layouts/_partials/comments/provider/disqusjs.html new file mode 100644 index 0000000..f4f4c03 --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/disqusjs.html @@ -0,0 +1,65 @@ +{{- $pc := .Site.Config.Privacy.Disqus -}} +{{- $disqusjs := .Site.Params.Comments.disqusjs -}} +{{- if and (not $pc.Disable) (and $disqusjs.Shortname $disqusjs.ApiKey) -}} + +{{- $style := resources.Get "scss/partials/comments/disqusjs.scss" | toCSS | minify -}} + + +
+
+ + +
+{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/comments/provider/giscus.html b/themes/stack/layouts/_partials/comments/provider/giscus.html new file mode 100644 index 0000000..aa83c61 --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/giscus.html @@ -0,0 +1,52 @@ +{{- with .Site.Params.comments.giscus -}} + + +{{- end -}} diff --git a/themes/stack/layouts/_partials/comments/provider/gitalk.html b/themes/stack/layouts/_partials/comments/provider/gitalk.html new file mode 100644 index 0000000..ac77fc6 --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/gitalk.html @@ -0,0 +1,32 @@ +{{- with .Site.Params.comments.gitalk -}} +
+ + + + +{{ end }} diff --git a/themes/stack/layouts/_partials/comments/provider/remark42.html b/themes/stack/layouts/_partials/comments/provider/remark42.html new file mode 100644 index 0000000..ad45c6e --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/remark42.html @@ -0,0 +1,29 @@ +{{- with .Site.Params.comments.remark42 -}} +
+ +{{- end -}} diff --git a/themes/stack/layouts/_partials/comments/provider/twikoo.html b/themes/stack/layouts/_partials/comments/provider/twikoo.html new file mode 100644 index 0000000..ae8cdae --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/twikoo.html @@ -0,0 +1,61 @@ + +
+ + +{{- with .Site.Params.comments.twikoo -}} + +{{- end -}} diff --git a/themes/stack/layouts/_partials/comments/provider/utterances.html b/themes/stack/layouts/_partials/comments/provider/utterances.html new file mode 100644 index 0000000..b1e370b --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/utterances.html @@ -0,0 +1,46 @@ + + + + + diff --git a/themes/stack/layouts/_partials/comments/provider/vssue.html b/themes/stack/layouts/_partials/comments/provider/vssue.html new file mode 100644 index 0000000..911afe9 --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/vssue.html @@ -0,0 +1,27 @@ +{{- with .Site.Params.comments.vssue -}} + + +
+ + + + + +{{- end -}} diff --git a/themes/stack/layouts/_partials/comments/provider/waline.html b/themes/stack/layouts/_partials/comments/provider/waline.html new file mode 100644 index 0000000..30a09eb --- /dev/null +++ b/themes/stack/layouts/_partials/comments/provider/waline.html @@ -0,0 +1,34 @@ + + +
+ + +{{- with .Site.Params.comments.waline -}} +{{- $config := dict "el" "#waline" "dark" `html[data-scheme="dark"]` -}} +{{- $replaceKeys := dict "serverurl" "serverURL" "requiredmeta" "requiredMeta" "wordlimit" "wordLimit" "pagesize" "pageSize" "imageuploader" "imageUploader" "texrenderer" "texRenderer" "turnstilekey" "turnstileKey" -}} + +{{- range $key, $val := . -}} + {{- if ne $val nil -}} + {{- $replaceKey := index $replaceKeys $key -}} + {{- $k := default $key $replaceKey -}} + + {{- $config = merge $config (dict $k $val) -}} + {{- end -}} +{{- end -}} + + +{{- end -}} diff --git a/themes/stack/layouts/_partials/cookies/analytics.html b/themes/stack/layouts/_partials/cookies/analytics.html new file mode 100644 index 0000000..c4e19ef --- /dev/null +++ b/themes/stack/layouts/_partials/cookies/analytics.html @@ -0,0 +1,50 @@ +{{- if not site.Config.Privacy.GoogleAnalytics.Disable -}} + {{- with site.Config.Services.GoogleAnalytics.ID -}} + {{- if strings.HasPrefix (lower .) "ua-" -}} + {{- warnf "Google Analytics 4 (GA4) replaced Google Universal Analytics (UA) effective 1 July 2023. See https://support.google.com/analytics/answer/11583528. Create a GA4 property and data stream, then replace the Google Analytics ID in your site configuration with the new value." -}} + {{- else -}} + {{/* Consent-gated Google Analytics - only loads after analytics consent */}} + + {{- end -}} + {{- end -}} +{{- end -}} diff --git a/themes/stack/layouts/_partials/cookies/banner.html b/themes/stack/layouts/_partials/cookies/banner.html new file mode 100644 index 0000000..a5f7174 --- /dev/null +++ b/themes/stack/layouts/_partials/cookies/banner.html @@ -0,0 +1,68 @@ +{{- $cfg := .Site.Params.cookies -}} +{{- $categories := $cfg.categories -}} + + diff --git a/themes/stack/layouts/_partials/cookies/include.html b/themes/stack/layouts/_partials/cookies/include.html new file mode 100644 index 0000000..ff6c6bd --- /dev/null +++ b/themes/stack/layouts/_partials/cookies/include.html @@ -0,0 +1,9 @@ +{{- if .Site.Params.cookies.enabled -}} + {{/* Render the banner HTML */}} + {{ partial "cookies/banner.html" . }} + + {{/* Load the consent manager script */}} + {{- $opts := dict "minify" hugo.IsProduction -}} + {{- $script := resources.Get "ts/cookies.ts" | js.Build $opts | fingerprint -}} + +{{- end -}} diff --git a/themes/stack/layouts/_partials/data/description.html b/themes/stack/layouts/_partials/data/description.html new file mode 100644 index 0000000..7f6cc32 --- /dev/null +++ b/themes/stack/layouts/_partials/data/description.html @@ -0,0 +1,17 @@ + +{{ $description := .Site.Params.sidebar.subtitle }} + + +{{ if .Site.Params.description }} + {{ $description = .Site.Params.description }} +{{ end }} + +{{ if .Description }} + + {{ $description = .Description }} +{{ else if .IsPage }} + + {{ $description = .Summary }} +{{ end }} + +{{ return (replaceRE "\n" " " $description | plainify) }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/data/title.html b/themes/stack/layouts/_partials/data/title.html new file mode 100644 index 0000000..0e56f34 --- /dev/null +++ b/themes/stack/layouts/_partials/data/title.html @@ -0,0 +1,14 @@ +{{- $pageTitle := .Title -}} +{{- $siteTitle := .Site.Title -}} +{{- $paginator := partial "helper/paginator.html" . -}} + +{{- $title := slice -}} + +{{- if and $paginator $paginator.HasPrev -}} + + {{ $title = $title | append (printf "Page %d" $paginator.PageNumber) }} +{{- end -}} + +{{- $title = $title | append (default $siteTitle $pageTitle) -}} + +{{ return delimit $title " - " }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/footer/components/script.html b/themes/stack/layouts/_partials/footer/components/script.html new file mode 100644 index 0000000..ea4bebc --- /dev/null +++ b/themes/stack/layouts/_partials/footer/components/script.html @@ -0,0 +1,18 @@ +{{- $opts := dict + "minify" hugo.IsProduction + "params" (dict + "codeblock" (dict + "copy" (i18n "article.codeblock.copy") + "copied" (i18n "article.codeblock.copied") + ) + ) +-}} +{{- $script := resources.Get "ts/main.ts" | js.Build $opts | fingerprint -}} + + + +{{- with resources.Get "ts/custom.ts" -}} + {{/* Place your custom script in HUGO_SITE_FOLDER/assets/ts/custom.ts */}} + {{- $customScript := . | js.Build $opts | fingerprint -}} + +{{- end -}} diff --git a/themes/stack/layouts/_partials/footer/custom.html b/themes/stack/layouts/_partials/footer/custom.html new file mode 100644 index 0000000..e69de29 diff --git a/themes/stack/layouts/_partials/footer/footer.html b/themes/stack/layouts/_partials/footer/footer.html new file mode 100644 index 0000000..79372a9 --- /dev/null +++ b/themes/stack/layouts/_partials/footer/footer.html @@ -0,0 +1,23 @@ +{{- $ThemeVersion := "4.0.3" -}} +
+ + +
+ {{ with .Site.Params.footer.customText }} + {{ . | safeHTML }}
+ {{ end }} + + {{- $Generator := `Hugo` -}} + {{- $Theme := printf `Stack` $ThemeVersion -}} + {{- $DesignedBy := `Jimmy` -}} + + {{ T "footer.builtWith" (dict "Generator" $Generator) | safeHTML }}
+ {{ T "footer.designedBy" (dict "Theme" $Theme "DesignedBy" $DesignedBy) | safeHTML }} +
+
diff --git a/themes/stack/layouts/_partials/footer/include.html b/themes/stack/layouts/_partials/footer/include.html new file mode 100644 index 0000000..54d85a8 --- /dev/null +++ b/themes/stack/layouts/_partials/footer/include.html @@ -0,0 +1,3 @@ +{{ partial "cookies/include.html" . }} +{{ partialCached "footer/components/script.html" . }} +{{ partial "footer/custom.html" . }} diff --git a/themes/stack/layouts/_partials/head/colorScheme.html b/themes/stack/layouts/_partials/head/colorScheme.html new file mode 100644 index 0000000..04d63a5 --- /dev/null +++ b/themes/stack/layouts/_partials/head/colorScheme.html @@ -0,0 +1,39 @@ +{{- $defaultColorScheme := .Site.Params.colorScheme.default -}} +{{- if not .Site.Params.colorScheme.toggle -}} + {{/* If toggle is disabled, force default scheme */}} + +{{- else -}} + {{/* Otherwise set to default scheme only if no preference is set by user */}} + +{{- end -}} + + diff --git a/themes/stack/layouts/_partials/head/custom-font.html b/themes/stack/layouts/_partials/head/custom-font.html new file mode 100644 index 0000000..8ab9cb2 --- /dev/null +++ b/themes/stack/layouts/_partials/head/custom-font.html @@ -0,0 +1,4 @@ + + + + diff --git a/themes/stack/layouts/_partials/head/custom.html b/themes/stack/layouts/_partials/head/custom.html new file mode 100644 index 0000000..e69de29 diff --git a/themes/stack/layouts/_partials/head/head.html b/themes/stack/layouts/_partials/head/head.html new file mode 100644 index 0000000..00f31d5 --- /dev/null +++ b/themes/stack/layouts/_partials/head/head.html @@ -0,0 +1,37 @@ + + + +{{- $description := partialCached "data/description" . .RelPermalink -}} + +{{ with .Params.Keywords }}{{ end }} + +{{- $title := partial "data/title" . -}} +{{ $title }} + +{{ with .Params.canonicalUrl }} + +{{ else }} + +{{ end }} + +{{- partial "head/custom-font.html" . -}} +{{- partial "head/style.html" . -}} +{{- partial "head/script.html" . -}} +{{- partial "head/opengraph/include.html" . -}} + +{{- range .AlternativeOutputFormats -}} + +{{- end -}} + +{{- $favicon := partial "helper/image" (dict "Image" .Site.Params.favicon "Resources" resources "Context" .) -}} +{{ with $favicon }} + +{{ end }} + +{{- if .Site.Params.cookies.enabled -}} + {{- partial "cookies/analytics.html" . -}} +{{- else -}} + {{- template "_internal/google_analytics.html" . -}} +{{- end -}} + +{{- partial "head/custom.html" . -}} diff --git a/themes/stack/layouts/_partials/head/opengraph/include.html b/themes/stack/layouts/_partials/head/opengraph/include.html new file mode 100644 index 0000000..3a43f48 --- /dev/null +++ b/themes/stack/layouts/_partials/head/opengraph/include.html @@ -0,0 +1,2 @@ +{{ partial "head/opengraph/provider/base" . }} +{{ partial "head/opengraph/provider/twitter" . }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/head/opengraph/provider/base.html b/themes/stack/layouts/_partials/head/opengraph/provider/base.html new file mode 100644 index 0000000..9fa2ab0 --- /dev/null +++ b/themes/stack/layouts/_partials/head/opengraph/provider/base.html @@ -0,0 +1,43 @@ +{{- $title := partialCached "data/title" . .RelPermalink -}} +{{- $description := partialCached "data/description" . .RelPermalink -}} + + + + + + + +{{- with .Params.locale -}} + +{{- end -}} + +{{- if .IsPage -}} + + {{- range .Params.tags -}} + + {{- end -}} +{{- end -}} + +{{- if .IsPage -}} + {{- if not .Date.IsZero -}} + + {{- end -}} + {{- if not .Lastmod.IsZero -}} + + {{- end -}} +{{- else -}} + {{- if not .Site.Lastmod.IsZero -}} + + {{- end -}} +{{- end -}} + +{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }} +{{- if $image -}} + +{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/head/opengraph/provider/twitter.html b/themes/stack/layouts/_partials/head/opengraph/provider/twitter.html new file mode 100644 index 0000000..111d266 --- /dev/null +++ b/themes/stack/layouts/_partials/head/opengraph/provider/twitter.html @@ -0,0 +1,16 @@ +{{- with .Site.Params.opengraph.twitter.site -}} + + +{{- end -}} + +{{- $title := partialCached "data/title" . .RelPermalink -}} +{{- $description := partialCached "data/description" . .RelPermalink -}} + + + + +{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}} +{{- if $image -}} + + +{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/head/script.html b/themes/stack/layouts/_partials/head/script.html new file mode 100644 index 0000000..e69de29 diff --git a/themes/stack/layouts/_partials/head/style.html b/themes/stack/layouts/_partials/head/style.html new file mode 100644 index 0000000..30ca26e --- /dev/null +++ b/themes/stack/layouts/_partials/head/style.html @@ -0,0 +1,3 @@ +{{ $sass := resources.Get "scss/style.scss" }} +{{ $style := $sass | toCSS | minify | resources.Fingerprint "sha256" }} + \ No newline at end of file diff --git a/themes/stack/layouts/_partials/helper/color-from-str.html b/themes/stack/layouts/_partials/helper/color-from-str.html new file mode 100644 index 0000000..6fc00cb --- /dev/null +++ b/themes/stack/layouts/_partials/helper/color-from-str.html @@ -0,0 +1,13 @@ +{{/* + This helper is used to generate a background color and text color from a string + Used to generate deterministic colors for tags, categories, etc. +*/}} + +{{- $str := . -}} +{{- $hash := hash.FNV32a $str -}} +{{- $h := mod $hash 360 -}} + +{{- $backgroundColor := printf "hsl(%d, 60%%, 84%%)" $h -}} +{{- $textColor := printf "hsl(%d, 60%%, 15%%)" $h -}} + +{{- return dict "BackgroundColor" $backgroundColor "TextColor" $textColor "Hue" $h -}} diff --git a/themes/stack/layouts/_partials/helper/external.html b/themes/stack/layouts/_partials/helper/external.html new file mode 100644 index 0000000..cd0e1fd --- /dev/null +++ b/themes/stack/layouts/_partials/helper/external.html @@ -0,0 +1,29 @@ +{{- $List := index hugo.Data.external .Namespace -}} +{{- with $List -}} + {{- range . -}} + {{- if eq .type "script" -}} + + {{- else if eq .type "style" -}} + + {{- else -}} + {{- errorf "Error: unknown external resource type: %s" .type -}} + {{- end -}} + {{- end -}} +{{- else -}} + {{- errorf "Error: external resource '%s' is not found" .Namespace -}} +{{- end -}} diff --git a/themes/stack/layouts/_partials/helper/icon.html b/themes/stack/layouts/_partials/helper/icon.html new file mode 100644 index 0000000..72162e8 --- /dev/null +++ b/themes/stack/layouts/_partials/helper/icon.html @@ -0,0 +1,6 @@ +{{- $iconFile := resources.GetMatch (printf "icons/%s.svg" .) -}} +{{- if $iconFile -}} + {{- $iconFile.Content | safeHTML -}} +{{- else -}} + {{- errorf "Error: icon '%s.svg' is not found under 'assets/icons' folder" . -}} +{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/helper/image.html b/themes/stack/layouts/_partials/helper/image.html new file mode 100644 index 0000000..51e71ff --- /dev/null +++ b/themes/stack/layouts/_partials/helper/image.html @@ -0,0 +1,72 @@ +/// Params: +/// Resources: Where to search for images +/// Image: Image URL +/// Context: page context (used to access the configuration) + +/// Returns: +/// Resource: Hugo image resource object +/// Permalink: Image URL +/// Height: Image height +/// Width: Image width + +{{ $result := dict }} + +{{ if .Image }} + {{ $url := urls.Parse .Image }} + {{ $permalink := .Image }} + + {{ $result = dict + "Resource" nil + "Permalink" $permalink + "Height" nil + "Width" nil + }} + {{ $resource := dict }} + {{ $local := true }} + + {{ if not (in (slice "https" "http") $url.Scheme) }} + {{/* Local image */}} + {{ $resource = .Resources.Get (printf "%s" (.Image | safeURL)) }} + {{ else }} + {{/* Remote image */}} + {{ with try (resources.GetRemote $url (dict "timeout" .Context.Site.Params.imageProcessing.external.timeout)) }} + {{ with .Err }} + {{ warnf "Failed to fetch remote resource %q: %s" $url . }} + {{ else with .Value }} + {{ $resource = . }} + {{ $local = false }} + {{ else }} + {{ warnf "Unable to get remote resource %q" $url }} + {{ end }} + {{ end }} + {{ end }} + + {{ if $resource }} + {{ if and .Context.Site.Params.imageProcessing.autoOrient (reflect.IsImageResourceProcessable $resource) }} + {{ $filter := images.AutoOrient }} + {{ $resource = $resource | images.Filter $filter }} + {{ end }} + + {{ if $local }} + {{ $permalink = $resource.RelPermalink }} + {{ end }} + + {{ if reflect.IsImageResourceWithMeta $resource }} + {{ $result = dict + "Resource" $resource + "Permalink" $permalink + "Height" $resource.Height + "Width" $resource.Width + }} + {{ else }} + {{ $result = dict + "Resource" $resource + "Permalink" $permalink + "Height" nil + "Width" nil + }} + {{ end }} + {{ end }} +{{ end }} + +{{ return $result }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/helper/pages-sort.html b/themes/stack/layouts/_partials/helper/pages-sort.html new file mode 100644 index 0000000..9f767c6 --- /dev/null +++ b/themes/stack/layouts/_partials/helper/pages-sort.html @@ -0,0 +1,7 @@ +{{- $pages := .Pages -}} + +{{- if eq .Context.Site.Params.SortBy "lastmod" -}} + {{- $pages = $pages.ByLastmod.Reverse -}} +{{- end -}} + +{{- return $pages -}} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/helper/pages.html b/themes/stack/layouts/_partials/helper/pages.html new file mode 100644 index 0000000..bca070a --- /dev/null +++ b/themes/stack/layouts/_partials/helper/pages.html @@ -0,0 +1,16 @@ +{{- $pages := .Pages -}} + +{{- if .IsHome -}} + {{- $pages = where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}} +{{- else if or (eq .Kind "section") (eq .Kind "taxonomy") (eq .Kind "term") -}} + {{- $subsections := .Sections -}} + {{- $pages = .Pages | complement $subsections -}} + + {{- if eq (len $pages) 0 -}} + {{- $pages = $subsections -}} + {{- end -}} +{{- end -}} + +{{- $pages := partial "helper/pages-sort.html" (dict "Pages" $pages "Context" .) -}} + +{{- return $pages -}} diff --git a/themes/stack/layouts/_partials/helper/paginator.html b/themes/stack/layouts/_partials/helper/paginator.html new file mode 100644 index 0000000..bba32a3 --- /dev/null +++ b/themes/stack/layouts/_partials/helper/paginator.html @@ -0,0 +1,8 @@ +{{- $result := slice -}} + +{{- if or .IsHome (eq .Kind "section") (eq .Kind "taxonomy") (eq .Kind "term") -}} + {{- $pages := partial "helper/pages.html" . -}} + {{- $result = .Paginate $pages -}} +{{- end -}} + +{{- return $result -}} diff --git a/themes/stack/layouts/_partials/helper/responsive-image.html b/themes/stack/layouts/_partials/helper/responsive-image.html new file mode 100644 index 0000000..63cc2de --- /dev/null +++ b/themes/stack/layouts/_partials/helper/responsive-image.html @@ -0,0 +1,33 @@ +{{- /* +Params: + Resource: Hugo image resource object (Optional, for processing) + Widths: Slice of widths to generate for srcset + Attributes: Map of HTML attributes for the tag +*/ -}} + +{{- $resource := .Resource -}} +{{- $widths := .Widths -}} +{{- $attributes := .Attributes | default dict -}} + +{{/* Generate srcset if Resource and Widths are provided */}} +{{- if and $widths $resource (reflect.IsImageResourceProcessable $resource) -}} + {{- $srcset := slice -}} + {{- range $widths -}} + {{- if lt . $resource.Width -}} + {{- $resized := $resource.Resize (printf "%dx" .) -}} + {{- $srcset = $srcset | append (printf "%s %dw" $resized.RelPermalink .) -}} + {{- end -}} + {{- end -}} + + {{- if gt (len $srcset) 0 -}} + {{- $permalink := default $resource.RelPermalink (index $attributes "src") -}} + {{- $srcset = $srcset | append (printf "%s %dw" $permalink $resource.Width) -}} + {{- $attributes = merge $attributes (dict "srcset" (delimit $srcset ", ")) -}} + {{- end -}} +{{- end -}} + + \ No newline at end of file diff --git a/themes/stack/layouts/_partials/helper/thumbnail-image.html b/themes/stack/layouts/_partials/helper/thumbnail-image.html new file mode 100644 index 0000000..442371b --- /dev/null +++ b/themes/stack/layouts/_partials/helper/thumbnail-image.html @@ -0,0 +1,41 @@ +{{- /* +Params: + Resource: Hugo image resource object (Optional, for processing) + Width: Image width (Required) + Height: Image height (Required) + Resize: Whether to perform resize (Optional, default: false) + Attributes: Map of HTML attributes for the tag +*/ -}} + +{{- $resource := .Resource -}} +{{- $width := .Width -}} +{{- $height := .Height -}} +{{- $resize := .Resize -}} +{{- $attributes := .Attributes | default dict -}} + +{{- if and $resize $resource (reflect.IsImageResourceProcessable $resource) $width $height -}} + {{/* Create thumbnail with 1x/2x descriptors */}} + {{- $srcset := slice -}} + {{- range (slice 1 2) -}} + {{- $w := mul $width . -}} + {{- $h := mul $height . -}} + {{- if and (le $w $resource.Width) (le $h $resource.Height) -}} + {{- $resized := $resource.Fill (printf "%dx%d" $w $h) -}} + {{- $srcset = $srcset | append (printf "%s %dx" $resized.RelPermalink .) -}} + + {{- if eq . 1 -}} + {{- $attributes = merge $attributes (dict "src" $resized.RelPermalink) -}} + {{- end -}} + {{- end -}} + {{- end -}} + + {{- if gt (len $srcset) 0 -}} + {{- $attributes = merge $attributes (dict "width" $width "height" $height "srcset" (delimit $srcset ", ")) -}} + {{- end -}} +{{- end -}} + + diff --git a/themes/stack/layouts/_partials/pagination.html b/themes/stack/layouts/_partials/pagination.html new file mode 100644 index 0000000..abfb64a --- /dev/null +++ b/themes/stack/layouts/_partials/pagination.html @@ -0,0 +1,142 @@ +{{- $pag := $.Paginator -}} +{{- if gt $pag.TotalPages 1 -}} + {{- /* Find page 2's URL to extract the pagination base path for JavaScript */ -}} + {{- $page2Url := "" -}} + {{- range $pag.Pagers -}} + {{- if eq .PageNumber 2 -}} + {{- $page2Url = .URL -}} + {{- end -}} + {{- end -}} + + + + +
+
+ + (1 - {{ $pag.TotalPages }}) +
+
+ + +
+
+ Enter + {{ i18n "pagination.pressEnter" | default "Press Enter to jump" }} +
+
+
+{{- end -}} diff --git a/themes/stack/layouts/_partials/sidebar/left.html b/themes/stack/layouts/_partials/sidebar/left.html new file mode 100644 index 0000000..46f0647 --- /dev/null +++ b/themes/stack/layouts/_partials/sidebar/left.html @@ -0,0 +1,95 @@ + diff --git a/themes/stack/layouts/_partials/sidebar/right.html b/themes/stack/layouts/_partials/sidebar/right.html new file mode 100644 index 0000000..c27e40b --- /dev/null +++ b/themes/stack/layouts/_partials/sidebar/right.html @@ -0,0 +1,16 @@ +{{- $scope := default "homepage" .Scope -}} +{{- $context := .Context -}} +{{- with (index .Context.Site.Params.widgets $scope) -}} + +{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/widget/archives.html b/themes/stack/layouts/_partials/widget/archives.html new file mode 100644 index 0000000..c2ea933 --- /dev/null +++ b/themes/stack/layouts/_partials/widget/archives.html @@ -0,0 +1,37 @@ +{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "archives") -}} +{{- $context := .Context -}} +{{- $limit := default 5 .Params.limit -}} +{{- if $query -}} + {{- $archivesPage := index $query 0 -}} +
+
+ {{ partial "helper/icon" "infinity" }} +
+

+ + {{ T "widget.archives.title" }} + +

+ + {{ $pages := partial "helper/pages.html" $context.Site.Home }} + {{ $archives := $pages.GroupByDate "2006" }} + +
+ {{ range $index, $item := first (add $limit 1) ($archives) }} + {{- $id := lower (replace $item.Key " " "-") -}} + + {{ end }} +
+
+{{- else -}} + {{- warnf "Archives page not found. Create a page with layout: archives." -}} +{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/widget/categories.html b/themes/stack/layouts/_partials/widget/categories.html new file mode 100644 index 0000000..7179b77 --- /dev/null +++ b/themes/stack/layouts/_partials/widget/categories.html @@ -0,0 +1,2 @@ +{{ $params := merge (dict "taxonomy" "categories" "title" (T "widget.categoriesCloud.title") "icon" "categories") .Params }} +{{ partial "widget/taxonomy" (dict "Context" .Context "Params" $params) }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/widget/search.html b/themes/stack/layouts/_partials/widget/search.html new file mode 100644 index 0000000..010ddbe --- /dev/null +++ b/themes/stack/layouts/_partials/widget/search.html @@ -0,0 +1,16 @@ +{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "search") -}} +{{- if $query -}} + {{- $searchPage := index $query 0 -}} +
+

+ + + + +

+
+{{- else -}} + {{- warnf "Search page not found. Create a page with layout: search." -}} +{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/widget/tag-cloud.html b/themes/stack/layouts/_partials/widget/tag-cloud.html new file mode 100644 index 0000000..88c3e46 --- /dev/null +++ b/themes/stack/layouts/_partials/widget/tag-cloud.html @@ -0,0 +1,2 @@ +{{ $params := merge (dict "taxonomy" "tags" "title" (T "widget.tagCloud.title") "icon" "tag") .Params }} +{{ partial "widget/taxonomy" (dict "Context" .Context "Params" $params) }} \ No newline at end of file diff --git a/themes/stack/layouts/_partials/widget/taxonomy.html b/themes/stack/layouts/_partials/widget/taxonomy.html new file mode 100644 index 0000000..c6fca84 --- /dev/null +++ b/themes/stack/layouts/_partials/widget/taxonomy.html @@ -0,0 +1,39 @@ +{{- $context := .Context -}} + +{{- if not .Params.taxonomy -}} + {{- warnf "Taxonomy field is required" -}} +{{- end -}} + +{{- $taxonomy := index $context.Site.Taxonomies .Params.taxonomy -}} +{{- if not $taxonomy -}} + {{- warnf "Taxonomy %s not found" .Params.taxonomy -}} +{{- end -}} + +{{- $taxonomyPage := $taxonomy.Page -}} +{{- $widgetTitle := .Params.title -}} +{{- $limit := default 10 .Params.limit -}} +{{- $icon := default .Params.taxonomy .Params.icon -}} +{{- $showLink := default true .Params.showLink -}} + +
+
+ {{ partial "helper/icon" $icon }} +
+

+ {{ if and $showLink $taxonomyPage }} + + {{ $widgetTitle }} + + {{ else }} + {{ $widgetTitle }} + {{ end }} +

+ +
+ {{ range first $limit $taxonomy.ByCount }} + + {{ .Page.Title }} + + {{ end }} +
+
diff --git a/themes/stack/layouts/_partials/widget/toc.html b/themes/stack/layouts/_partials/widget/toc.html new file mode 100644 index 0000000..22efe3e --- /dev/null +++ b/themes/stack/layouts/_partials/widget/toc.html @@ -0,0 +1,14 @@ +{{ if (.Context.Scratch.Get "TOCEnabled") }} +
+
+ {{ partial "helper/icon" "hash" }} +
+

{{ T "article.tableOfContents" }}

+ +
+ +
+
+{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/_shortcodes/bilibili.html b/themes/stack/layouts/_shortcodes/bilibili.html new file mode 100644 index 0000000..cb72e43 --- /dev/null +++ b/themes/stack/layouts/_shortcodes/bilibili.html @@ -0,0 +1,23 @@ +{{ $vid := (.Get 0) }} +{{ $videopage := default 1 (.Get 1) }} +{{ $basicQuery := querify "page" $videopage "high_quality" 1 "as_wide" 1 }} +{{ $videoQuery := "" }} + +{{ if strings.HasPrefix (lower $vid) "av" }} + {{ $videoQuery = querify "aid" (strings.TrimPrefix "av" (lower $vid)) }} +{{ else if strings.HasPrefix (lower $vid) "bv" }} + {{ $videoQuery = querify "bvid" $vid }} +{{ else }} +

Bilibili 视频av号或BV号错误!请检查视频av号或BV号是否正确

+

当前视频av或BV号:{{ $vid }},视频分P:{{ $videopage }}

+{{ end }} + +
+ +
diff --git a/themes/stack/layouts/_shortcodes/gitlab.html b/themes/stack/layouts/_shortcodes/gitlab.html new file mode 100644 index 0000000..b5a16cf --- /dev/null +++ b/themes/stack/layouts/_shortcodes/gitlab.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/themes/stack/layouts/_shortcodes/quote.html b/themes/stack/layouts/_shortcodes/quote.html new file mode 100644 index 0000000..09bb07c --- /dev/null +++ b/themes/stack/layouts/_shortcodes/quote.html @@ -0,0 +1,15 @@ +
+

{{ .Inner | markdownify }}

+ {{- if or (.Get "author") (.Get "source") -}} + + {{- if .Get "author" -}} + + {{- .Get "author" -}}{{- if .Get "source" -}}, {{ end -}} + + {{- end -}} + {{- with .Get "url" -}}{{- end -}} + {{ .Get "source" }} + {{- if .Get "url" -}}{{- end -}} + + {{- end -}} +
\ No newline at end of file diff --git a/themes/stack/layouts/_shortcodes/tencent.html b/themes/stack/layouts/_shortcodes/tencent.html new file mode 100644 index 0000000..463dec5 --- /dev/null +++ b/themes/stack/layouts/_shortcodes/tencent.html @@ -0,0 +1,10 @@ +{{ $vid := .Get 0 }} +
+ +
\ No newline at end of file diff --git a/themes/stack/layouts/_shortcodes/video.html b/themes/stack/layouts/_shortcodes/video.html new file mode 100644 index 0000000..0a5260c --- /dev/null +++ b/themes/stack/layouts/_shortcodes/video.html @@ -0,0 +1,15 @@ +{{- $src := .Get "src" | default (.Get 0) -}} +
+ +
diff --git a/themes/stack/layouts/_shortcodes/youtube.html b/themes/stack/layouts/_shortcodes/youtube.html new file mode 100644 index 0000000..2f85931 --- /dev/null +++ b/themes/stack/layouts/_shortcodes/youtube.html @@ -0,0 +1,13 @@ +{{- $pc := .Page.Site.Config.Privacy.YouTube -}} +{{- if not $pc.Disable -}} +{{- $ytHost := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" -}} +{{- $id := .Get "id" | default (.Get 0) -}} +
+ +
+{{ end -}} \ No newline at end of file diff --git a/themes/stack/layouts/archives.html b/themes/stack/layouts/archives.html new file mode 100644 index 0000000..e2853c1 --- /dev/null +++ b/themes/stack/layouts/archives.html @@ -0,0 +1,33 @@ +{{ define "body-class" }}template-archives{{ end }} +{{ define "main" }} +
+ {{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}} + {{- $terms := $taxonomy.Pages -}} + {{ if $terms }} +

{{ T "widget.categoriesCloud.title" }}

+
+
+ {{ range $terms }} + {{ partial "article-list/tile" . }} + {{ end }} +
+
+ {{ end }} +
+ + {{ $pages := partial "helper/pages.html" .Site.Home }} + + {{ range $pages.GroupByDate "2006" }} + {{ $id := lower (replace .Key " " "-") }} +
+

{{ .Key }}

+
+ {{ range .Pages }} + {{ partial "article-list/compact" . }} + {{ end }} +
+
+ {{ end }} + + {{ partialCached "footer/footer" . }} +{{ end }} diff --git a/themes/stack/layouts/baseof.html b/themes/stack/layouts/baseof.html new file mode 100644 index 0000000..c9a0b2b --- /dev/null +++ b/themes/stack/layouts/baseof.html @@ -0,0 +1,28 @@ + + + + {{- partial "head/head.html" . -}} + {{- block "head" . -}}{{ end }} + + + {{- partial "head/colorScheme" . -}} + + {{/* The container is wider when there's any activated widget */}} + {{- $hasWidget := false -}} + {{- range .Site.Params.widgets -}} + {{- if gt (len .) 0 -}} + {{- $hasWidget = true -}} + {{- end -}} + {{- end -}} +
+ {{- block "left-sidebar" . -}} + {{ partial "sidebar/left.html" . }} + {{- end -}} + {{- block "right-sidebar" . -}}{{ end }} +
+ {{- block "main" . }}{{- end }} +
+
+ {{ partial "footer/include.html" . }} + + diff --git a/themes/stack/layouts/home.html b/themes/stack/layouts/home.html new file mode 100644 index 0000000..d528ac1 --- /dev/null +++ b/themes/stack/layouts/home.html @@ -0,0 +1,16 @@ +{{ define "main" }} + {{- $paginator := partial "helper/paginator.html" . -}} + +
+ {{ range $index, $element := $paginator.Pages }} + {{ partial "article-list/default" . }} + {{ end }} +
+ + {{- partial "pagination.html" . -}} + {{- partial "footer/footer" . -}} +{{ end }} + +{{ define "right-sidebar" }} + {{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }} +{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/list.html b/themes/stack/layouts/list.html new file mode 100644 index 0000000..777bb41 --- /dev/null +++ b/themes/stack/layouts/list.html @@ -0,0 +1,78 @@ +{{ define "main" }} +
+

+ {{ if eq .Parent (.GetPage "/") }} + {{ T "list.section" }} + {{ else }} + {{ .Parent.Title }} + {{ end }} +

+ +
+
+

{{ T "list.page" (len .Pages) }}

+

{{ .Title }}

+ {{ with .Params.description }} +

{{ . }}

+ {{ end }} +
+ + {{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}} + {{ if $image }} +
+ {{ partial "helper/thumbnail-image" (dict + "Resource" $image.Resource + "Width" 60 + "Height" 60 + "Resize" .Site.Params.ImageProcessing.Thumbnail.Enabled + "Attributes" (dict + "src" $image.Permalink + "alt" .Title + "loading" "lazy" + "width" $image.Width + "height" $image.Height + ) + ) }} +
+ {{ end }} +
+
+ + {{- $pages := partial "helper/pages.html" . -}} + {{- $subsections := .Sections -}} + + {{- if and (eq (len $pages) (len $subsections)) (eq $pages $subsections) -}} + {{- $subsections = slice -}} + {{- end -}} + + {{- with $subsections -}} + + {{- end -}} + + {{/* List only pages that are not a subsection */}} + {{ $paginator := partial "helper/paginator.html" . }} + {{ with $paginator }} +
+ {{ range .Pages }} + {{ partial "article-list/compact" . }} + {{ end }} +
+ {{ end }} + + {{- partial "pagination.html" . -}} + + {{ partialCached "footer/footer" . }} +{{ end }} + +{{ define "right-sidebar" }} + {{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }} +{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/page/search.html b/themes/stack/layouts/page/search.html new file mode 100644 index 0000000..b2e056f --- /dev/null +++ b/themes/stack/layouts/page/search.html @@ -0,0 +1,33 @@ +{{ define "body-class" }}template-search{{ end }} +{{ define "head" }} + {{- with .OutputFormats.Get "json" -}} + + {{- end -}} +{{ end }} +{{ define "main" }} +
+

+ + +

+ + +
+ + + + + +{{- $opts := dict "minify" hugo.IsProduction "JSXFactory" "createElement" -}} +{{- $searchScript := resources.Get "ts/search.tsx" | js.Build $opts -}} + + +{{ partialCached "footer/footer" . }} +{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/page/search.json b/themes/stack/layouts/page/search.json new file mode 100644 index 0000000..e69868d --- /dev/null +++ b/themes/stack/layouts/page/search.json @@ -0,0 +1,15 @@ +{{- $pages := partial "helper/pages.html" .Site.Home -}} +{{- $result := slice -}} + +{{- range $pages -}} + {{- $data := dict "title" .Title "date" .Date "permalink" .RelPermalink "content" (.Plain) -}} + + {{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}} + {{- if $image -}} + {{- $data = merge $data (dict "image" $image.Permalink) -}} + {{- end -}} + + {{- $result = $result | append $data -}} +{{- end -}} + +{{ jsonify $result }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/article-list/compact.html b/themes/stack/layouts/partials/article-list/compact.html new file mode 100644 index 0000000..ec01bfd --- /dev/null +++ b/themes/stack/layouts/partials/article-list/compact.html @@ -0,0 +1,49 @@ +
+ +
+

+ {{- .Title -}} +

+ + {{- $description := .Params.description -}} + {{- $IsTerm := in .Kind "term" -}} + {{- if $IsTerm -}} + {{- $description = $description | default (T "list.page" (len .Pages)) -}} + {{- end -}} + {{- if or $description (not .Date.IsZero) -}} + {{/* + If it's a term, the date is extracted from the first page of the collection, which is not very useful. + In that case, we prefer to display the description (if set), or the number of pages within the collection. + */}} +
+ {{- if or $IsTerm .Date.IsZero -}} + {{- $description -}} + {{- else if not .Date.IsZero -}} + + {{- end }} +
+ {{- end -}} +
+ + {{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}} + {{ if $image }} +
+ {{ partial "helper/thumbnail-image" (dict + "Resource" $image.Resource + "Width" 60 + "Height" 60 + "Resize" .Site.Params.ImageProcessing.Thumbnail.Enabled + "Attributes" (dict + "src" $image.Permalink + "alt" .Title + "loading" "lazy" + "width" $image.Width + "height" $image.Height + ) + ) }} +
+ {{ end }} +
+
\ No newline at end of file diff --git a/themes/stack/layouts/partials/article-list/default.html b/themes/stack/layouts/partials/article-list/default.html new file mode 100644 index 0000000..0a1a4b0 --- /dev/null +++ b/themes/stack/layouts/partials/article-list/default.html @@ -0,0 +1,4 @@ +{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }} +
+ {{ partial "article/components/header" (dict "Page" . "IsList" true) }} +
\ No newline at end of file diff --git a/themes/stack/layouts/partials/article-list/tile.html b/themes/stack/layouts/partials/article-list/tile.html new file mode 100644 index 0000000..8e9df0f --- /dev/null +++ b/themes/stack/layouts/partials/article-list/tile.html @@ -0,0 +1,37 @@ +{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }} + \ No newline at end of file diff --git a/themes/stack/layouts/partials/article/article.html b/themes/stack/layouts/partials/article/article.html new file mode 100644 index 0000000..88d4f53 --- /dev/null +++ b/themes/stack/layouts/partials/article/article.html @@ -0,0 +1,27 @@ +
+ {{ partial "article/components/header" (dict "Page" . "IsList" false) }} + + {{ if .Scratch.Get "TOCEnabled" }} + + {{ end }} + + {{ partial "article/components/content" . }} + + {{ partial "article/components/footer" . }} + + {{ if or .Params.math .Site.Params.article.math }} + {{ partialCached "article/components/math.html" . }} + {{ end }} +
\ No newline at end of file diff --git a/themes/stack/layouts/partials/article/components/content.html b/themes/stack/layouts/partials/article/components/content.html new file mode 100644 index 0000000..61e536c --- /dev/null +++ b/themes/stack/layouts/partials/article/components/content.html @@ -0,0 +1,5 @@ +
+ + {{ $wrappedTable := printf "
${1}
" }} + {{ .Content | replaceRE "((?:.|\n)+?
)" $wrappedTable | safeHTML }} +
diff --git a/themes/stack/layouts/partials/article/components/details.html b/themes/stack/layouts/partials/article/components/details.html new file mode 100644 index 0000000..b7c6c14 --- /dev/null +++ b/themes/stack/layouts/partials/article/components/details.html @@ -0,0 +1,76 @@ +{{- $IsList := .IsList -}} +{{- $Page := .Page -}} +
+ {{ if $Page.Params.categories }} + + {{ end }} + +
+

+ + {{- $Page.Title -}} + +

+ + {{ with $Page.Params.description }} +

+ {{ . }} +

+ {{ end }} +
+ + {{ $showReadingTime := $Page.Params.readingTime | default ($Page.Site.Params.article.readingTime) }} + {{ $showDate := not $Page.Date.IsZero }} + {{ $showTime := or $showDate $showReadingTime }} + {{ $showTranslations := $Page.IsTranslated }} + {{ $showTags := and $Page.Site.Params.Article.List.ShowTags ($Page.GetTerms "tags") $IsList }} + + {{ if or $showTime $showTranslations $showTags }} +
+ {{ if $showTime }} +
+ {{ if $showDate }} + {{ partial "helper/icon" "date" }} + + {{ end }} + + {{ if $showReadingTime }} + {{ partial "helper/icon" "clock" }} + + {{ end }} +
+ {{ end }} + + {{ if $showTranslations }} +
+ {{ partial "helper/icon" "language" }} + {{ range $Page.Translations }} + {{ .Lang }} + {{ end }} +
+ {{ end }} + + {{ if $showTags }} + + {{ end }} +
+ {{ end }} +
diff --git a/themes/stack/layouts/partials/article/components/footer.html b/themes/stack/layouts/partials/article/components/footer.html new file mode 100644 index 0000000..7fecbac --- /dev/null +++ b/themes/stack/layouts/partials/article/components/footer.html @@ -0,0 +1,19 @@ +
+ {{ partial "article/components/tags" . }} + + {{ if and (.Site.Params.article.license.enabled) (not (eq .Params.license false)) }} +
+ {{ partial "helper/icon" "copyright" }} + {{ default .Site.Params.article.license.default .Params.license | markdownify }} +
+ {{ end }} + + {{- if ne .Lastmod .Date -}} +
+ {{ partial "helper/icon" "clock" }} + + {{ T "article.lastUpdatedOn" }} {{ .Lastmod | time.Format .Site.Params.dateFormat.lastUpdated }} + +
+ {{- end -}} +
diff --git a/themes/stack/layouts/partials/article/components/header.html b/themes/stack/layouts/partials/article/components/header.html new file mode 100644 index 0000000..0d65fc5 --- /dev/null +++ b/themes/stack/layouts/partials/article/components/header.html @@ -0,0 +1,25 @@ +{{- $IsList := .IsList -}} +{{- $Page := .Page -}} +
+ {{- $image := partial "helper/image" (dict "Image" $Page.Params.image "Resources" $Page.Resources "Context" $Page) -}} + {{ if $image }} +
+ + {{ partial "helper/responsive-image" (dict + "Resource" $image.Resource + "Widths" (cond $Page.Site.Params.ImageProcessing.Content.Enabled $Page.Site.Params.ImageProcessing.Content.Widths nil) + "Attributes" (dict + "src" $image.Permalink + "width" $image.Width + "height" $image.Height + "alt" (printf "Featured image of post %s" $Page.Title) + "loading" "lazy" + "sizes" "(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" + ) + ) }} + +
+ {{ end }} + + {{ partial "article/components/details" (dict "Page" $Page "IsList" $IsList) }} +
\ No newline at end of file diff --git a/themes/stack/layouts/partials/article/components/links.html b/themes/stack/layouts/partials/article/components/links.html new file mode 100644 index 0000000..a6fa768 --- /dev/null +++ b/themes/stack/layouts/partials/article/components/links.html @@ -0,0 +1,39 @@ +
+ {{ range $link := .Params.links }} +
+ +
+

+ {{- $link.title -}} +

+
+ {{ with $link.description }} + {{ . }} + {{ else }} + {{ $link.website }} + {{ end }} +
+
+ + {{- $image := partial "helper/image" (dict "Image" $link.image "Resources" $.Resources "Context" .) -}} + {{- with $image -}} +
+ {{ partial "helper/thumbnail-image" (dict + "Resource" .Resource + "Width" 60 + "Height" 60 + "Resize" $.Site.Params.ImageProcessing.Thumbnail.Enabled + "Attributes" (dict + "src" $image.Permalink + "alt" $link.title + "loading" "lazy" + "width" .Width + "height" .Height + ) + ) }} +
+ {{ end }} +
+
+ {{ end }} +
\ No newline at end of file diff --git a/themes/stack/layouts/partials/article/components/math.html b/themes/stack/layouts/partials/article/components/math.html new file mode 100644 index 0000000..a7ce654 --- /dev/null +++ b/themes/stack/layouts/partials/article/components/math.html @@ -0,0 +1,21 @@ +{{- partial "helper/external" (dict "Context" . "Namespace" "KaTeX") -}} + \ No newline at end of file diff --git a/themes/stack/layouts/partials/article/components/mermaid.html b/themes/stack/layouts/partials/article/components/mermaid.html new file mode 100644 index 0000000..dd75faf --- /dev/null +++ b/themes/stack/layouts/partials/article/components/mermaid.html @@ -0,0 +1,42 @@ +{{- if .Store.Get "hasMermaid" -}} +{{- $cfg := site.Params.article.mermaid | default dict -}} + +
+
+
+ + + + +
+ +
+
+
+
+
+ + +{{- $opts := dict "minify" hugo.IsProduction "format" "esm" -}} +{{- $script := resources.Get "ts/mermaid.ts" | js.Build $opts -}} +{{- $jsConfig := dict + "transparentBackground" ($cfg.transparentBackground | default false) + "lightTheme" ($cfg.lightTheme | default "default") + "darkTheme" ($cfg.darkTheme | default "dark") + "lightThemeVariables" ($cfg.lightThemeVariables | default dict) + "darkThemeVariables" ($cfg.darkThemeVariables | default dict) + "securityLevel" ($cfg.securityLevel | default "strict") + "look" ($cfg.look | default "classic") + "htmlLabels" (not (eq ($cfg.htmlLabels | default true) false)) +-}} +{{- with $cfg.maxTextSize }}{{ $jsConfig = merge $jsConfig (dict "maxTextSize" .) }}{{ end -}} +{{- with $cfg.maxEdges }}{{ $jsConfig = merge $jsConfig (dict "maxEdges" .) }}{{ end -}} +{{- with $cfg.fontSize }}{{ $jsConfig = merge $jsConfig (dict "fontSize" .) }}{{ end -}} +{{- with $cfg.fontFamily }}{{ $jsConfig = merge $jsConfig (dict "fontFamily" .) }}{{ end -}} +{{- with $cfg.curve }}{{ $jsConfig = merge $jsConfig (dict "curve" .) }}{{ end -}} +{{- with $cfg.logLevel }}{{ $jsConfig = merge $jsConfig (dict "logLevel" .) }}{{ end -}} + +{{- end -}} diff --git a/themes/stack/layouts/partials/article/components/photoswipe.html b/themes/stack/layouts/partials/article/components/photoswipe.html new file mode 100644 index 0000000..1694a9f --- /dev/null +++ b/themes/stack/layouts/partials/article/components/photoswipe.html @@ -0,0 +1,32 @@ +{{- $opts := dict "minify" hugo.IsProduction "format" "esm" -}} +{{- $galleryScript := resources.Get "ts/gallery.ts" | js.Build $opts -}} + +{{ $style := hugo.Data.external.PhotoSwipe.Style }} +{{ $core := hugo.Data.external.PhotoSwipe.Core }} +{{ $lightbox := hugo.Data.external.PhotoSwipe.Lightbox }} + + diff --git a/themes/stack/layouts/partials/article/components/related-content.html b/themes/stack/layouts/partials/article/components/related-content.html new file mode 100644 index 0000000..c121303 --- /dev/null +++ b/themes/stack/layouts/partials/article/components/related-content.html @@ -0,0 +1,13 @@ +{{ $related := (.Site.RegularPages.Related .) | first 5 }} +{{ with $related }} + +{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/article/components/tags.html b/themes/stack/layouts/partials/article/components/tags.html new file mode 100644 index 0000000..aae38b4 --- /dev/null +++ b/themes/stack/layouts/partials/article/components/tags.html @@ -0,0 +1,7 @@ +{{ if .Params.Tags }} + +{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/article/components/toc.html b/themes/stack/layouts/partials/article/components/toc.html new file mode 100644 index 0000000..e572226 --- /dev/null +++ b/themes/stack/layouts/partials/article/components/toc.html @@ -0,0 +1,6 @@ +{{/* Remove from .TableOfContents */}} +{{ $toc := .TableOfContents }} +{{ $toc = replace $toc "" "" }} + +{{ return $toc }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/comments/include.html b/themes/stack/layouts/partials/comments/include.html new file mode 100644 index 0000000..ac329e9 --- /dev/null +++ b/themes/stack/layouts/partials/comments/include.html @@ -0,0 +1,28 @@ +{{/* + Comments include entry. + - If functional cookie consent is required, render a placeholder + template and load commentsConsent.ts. + - Otherwise, render provider partial directly. +*/}} +{{ if .Site.Params.comments.enabled }} + {{- $needsConsent := and .Site.Params.cookies.enabled .Site.Params.cookies.categories.functional -}} + {{- if $needsConsent -}} + {{/* Consent-gated comments - show placeholder until functional consent */}} + + + + {{- $opts := dict "minify" hugo.IsProduction -}} + {{/* commentsConsent.ts is intentionally built and loaded here as an independent entry */}} + {{- $commentsScript := resources.Get "ts/commentsConsent.ts" | js.Build $opts | fingerprint -}} + + {{- else -}} + {{/* No consent required - load comments normally */}} + {{ partial (printf "comments/provider/%s" .Site.Params.comments.provider) . }} + {{- end -}} +{{ end }} diff --git a/themes/stack/layouts/partials/comments/provider/artalk.html b/themes/stack/layouts/partials/comments/provider/artalk.html new file mode 100644 index 0000000..2cfbc8e --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/artalk.html @@ -0,0 +1,30 @@ +{{- $style := resources.Get "scss/partials/comments/artalk.scss" | toCSS | minify -}} +{{- with .Site.Params.comments.artalk -}} + + + +
+ + +{{- end -}} diff --git a/themes/stack/layouts/partials/comments/provider/beaudar.html b/themes/stack/layouts/partials/comments/provider/beaudar.html new file mode 100644 index 0000000..761801f --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/beaudar.html @@ -0,0 +1,46 @@ + + + + + diff --git a/themes/stack/layouts/partials/comments/provider/cactus.html b/themes/stack/layouts/partials/comments/provider/cactus.html new file mode 100644 index 0000000..ae172d3 --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/cactus.html @@ -0,0 +1,29 @@ +{{- with .Site.Params.comments.cactus -}} +{{- partial "helper/external" (dict "Context" $ "Namespace" "Cactus") -}} + + + +
+ + +{{- end -}} diff --git a/themes/stack/layouts/partials/comments/provider/comentario.html b/themes/stack/layouts/partials/comments/provider/comentario.html new file mode 100644 index 0000000..a78323d --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/comentario.html @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/themes/stack/layouts/partials/comments/provider/cusdis.html b/themes/stack/layouts/partials/comments/provider/cusdis.html new file mode 100644 index 0000000..d0ed7f1 --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/cusdis.html @@ -0,0 +1,27 @@ +{{- $host := .Site.Params.comments.cusdis.host -}} +
+ + + \ No newline at end of file diff --git a/themes/stack/layouts/partials/comments/provider/disqus.html b/themes/stack/layouts/partials/comments/provider/disqus.html new file mode 100644 index 0000000..32e6197 --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/disqus.html @@ -0,0 +1,22 @@ +
+ {{ partial "disqus.html" . }} +
+ + + + \ No newline at end of file diff --git a/themes/stack/layouts/partials/comments/provider/disqusjs.html b/themes/stack/layouts/partials/comments/provider/disqusjs.html new file mode 100644 index 0000000..f4f4c03 --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/disqusjs.html @@ -0,0 +1,65 @@ +{{- $pc := .Site.Config.Privacy.Disqus -}} +{{- $disqusjs := .Site.Params.Comments.disqusjs -}} +{{- if and (not $pc.Disable) (and $disqusjs.Shortname $disqusjs.ApiKey) -}} + +{{- $style := resources.Get "scss/partials/comments/disqusjs.scss" | toCSS | minify -}} + + +
+
+ + +
+{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/partials/comments/provider/giscus.html b/themes/stack/layouts/partials/comments/provider/giscus.html new file mode 100644 index 0000000..aa83c61 --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/giscus.html @@ -0,0 +1,52 @@ +{{- with .Site.Params.comments.giscus -}} + + +{{- end -}} diff --git a/themes/stack/layouts/partials/comments/provider/gitalk.html b/themes/stack/layouts/partials/comments/provider/gitalk.html new file mode 100644 index 0000000..ac77fc6 --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/gitalk.html @@ -0,0 +1,32 @@ +{{- with .Site.Params.comments.gitalk -}} +
+ + + + +{{ end }} diff --git a/themes/stack/layouts/partials/comments/provider/remark42.html b/themes/stack/layouts/partials/comments/provider/remark42.html new file mode 100644 index 0000000..ad45c6e --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/remark42.html @@ -0,0 +1,29 @@ +{{- with .Site.Params.comments.remark42 -}} +
+ +{{- end -}} diff --git a/themes/stack/layouts/partials/comments/provider/twikoo.html b/themes/stack/layouts/partials/comments/provider/twikoo.html new file mode 100644 index 0000000..ae8cdae --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/twikoo.html @@ -0,0 +1,61 @@ + +
+ + +{{- with .Site.Params.comments.twikoo -}} + +{{- end -}} diff --git a/themes/stack/layouts/partials/comments/provider/utterances.html b/themes/stack/layouts/partials/comments/provider/utterances.html new file mode 100644 index 0000000..b1e370b --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/utterances.html @@ -0,0 +1,46 @@ + + + + + diff --git a/themes/stack/layouts/partials/comments/provider/vssue.html b/themes/stack/layouts/partials/comments/provider/vssue.html new file mode 100644 index 0000000..911afe9 --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/vssue.html @@ -0,0 +1,27 @@ +{{- with .Site.Params.comments.vssue -}} + + +
+ + + + + +{{- end -}} diff --git a/themes/stack/layouts/partials/comments/provider/waline.html b/themes/stack/layouts/partials/comments/provider/waline.html new file mode 100644 index 0000000..30a09eb --- /dev/null +++ b/themes/stack/layouts/partials/comments/provider/waline.html @@ -0,0 +1,34 @@ + + +
+ + +{{- with .Site.Params.comments.waline -}} +{{- $config := dict "el" "#waline" "dark" `html[data-scheme="dark"]` -}} +{{- $replaceKeys := dict "serverurl" "serverURL" "requiredmeta" "requiredMeta" "wordlimit" "wordLimit" "pagesize" "pageSize" "imageuploader" "imageUploader" "texrenderer" "texRenderer" "turnstilekey" "turnstileKey" -}} + +{{- range $key, $val := . -}} + {{- if ne $val nil -}} + {{- $replaceKey := index $replaceKeys $key -}} + {{- $k := default $key $replaceKey -}} + + {{- $config = merge $config (dict $k $val) -}} + {{- end -}} +{{- end -}} + + +{{- end -}} diff --git a/themes/stack/layouts/partials/cookies/analytics.html b/themes/stack/layouts/partials/cookies/analytics.html new file mode 100644 index 0000000..c4e19ef --- /dev/null +++ b/themes/stack/layouts/partials/cookies/analytics.html @@ -0,0 +1,50 @@ +{{- if not site.Config.Privacy.GoogleAnalytics.Disable -}} + {{- with site.Config.Services.GoogleAnalytics.ID -}} + {{- if strings.HasPrefix (lower .) "ua-" -}} + {{- warnf "Google Analytics 4 (GA4) replaced Google Universal Analytics (UA) effective 1 July 2023. See https://support.google.com/analytics/answer/11583528. Create a GA4 property and data stream, then replace the Google Analytics ID in your site configuration with the new value." -}} + {{- else -}} + {{/* Consent-gated Google Analytics - only loads after analytics consent */}} + + {{- end -}} + {{- end -}} +{{- end -}} diff --git a/themes/stack/layouts/partials/cookies/banner.html b/themes/stack/layouts/partials/cookies/banner.html new file mode 100644 index 0000000..a5f7174 --- /dev/null +++ b/themes/stack/layouts/partials/cookies/banner.html @@ -0,0 +1,68 @@ +{{- $cfg := .Site.Params.cookies -}} +{{- $categories := $cfg.categories -}} + + diff --git a/themes/stack/layouts/partials/cookies/include.html b/themes/stack/layouts/partials/cookies/include.html new file mode 100644 index 0000000..ff6c6bd --- /dev/null +++ b/themes/stack/layouts/partials/cookies/include.html @@ -0,0 +1,9 @@ +{{- if .Site.Params.cookies.enabled -}} + {{/* Render the banner HTML */}} + {{ partial "cookies/banner.html" . }} + + {{/* Load the consent manager script */}} + {{- $opts := dict "minify" hugo.IsProduction -}} + {{- $script := resources.Get "ts/cookies.ts" | js.Build $opts | fingerprint -}} + +{{- end -}} diff --git a/themes/stack/layouts/partials/data/description.html b/themes/stack/layouts/partials/data/description.html new file mode 100644 index 0000000..7f6cc32 --- /dev/null +++ b/themes/stack/layouts/partials/data/description.html @@ -0,0 +1,17 @@ + +{{ $description := .Site.Params.sidebar.subtitle }} + + +{{ if .Site.Params.description }} + {{ $description = .Site.Params.description }} +{{ end }} + +{{ if .Description }} + + {{ $description = .Description }} +{{ else if .IsPage }} + + {{ $description = .Summary }} +{{ end }} + +{{ return (replaceRE "\n" " " $description | plainify) }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/data/title.html b/themes/stack/layouts/partials/data/title.html new file mode 100644 index 0000000..0e56f34 --- /dev/null +++ b/themes/stack/layouts/partials/data/title.html @@ -0,0 +1,14 @@ +{{- $pageTitle := .Title -}} +{{- $siteTitle := .Site.Title -}} +{{- $paginator := partial "helper/paginator.html" . -}} + +{{- $title := slice -}} + +{{- if and $paginator $paginator.HasPrev -}} + + {{ $title = $title | append (printf "Page %d" $paginator.PageNumber) }} +{{- end -}} + +{{- $title = $title | append (default $siteTitle $pageTitle) -}} + +{{ return delimit $title " - " }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/footer/components/script.html b/themes/stack/layouts/partials/footer/components/script.html new file mode 100644 index 0000000..ea4bebc --- /dev/null +++ b/themes/stack/layouts/partials/footer/components/script.html @@ -0,0 +1,18 @@ +{{- $opts := dict + "minify" hugo.IsProduction + "params" (dict + "codeblock" (dict + "copy" (i18n "article.codeblock.copy") + "copied" (i18n "article.codeblock.copied") + ) + ) +-}} +{{- $script := resources.Get "ts/main.ts" | js.Build $opts | fingerprint -}} + + + +{{- with resources.Get "ts/custom.ts" -}} + {{/* Place your custom script in HUGO_SITE_FOLDER/assets/ts/custom.ts */}} + {{- $customScript := . | js.Build $opts | fingerprint -}} + +{{- end -}} diff --git a/themes/stack/layouts/partials/footer/custom.html b/themes/stack/layouts/partials/footer/custom.html new file mode 100644 index 0000000..e69de29 diff --git a/themes/stack/layouts/partials/footer/footer.html b/themes/stack/layouts/partials/footer/footer.html new file mode 100644 index 0000000..79372a9 --- /dev/null +++ b/themes/stack/layouts/partials/footer/footer.html @@ -0,0 +1,23 @@ +{{- $ThemeVersion := "4.0.3" -}} +
+ + +
+ {{ with .Site.Params.footer.customText }} + {{ . | safeHTML }}
+ {{ end }} + + {{- $Generator := `Hugo` -}} + {{- $Theme := printf `Stack` $ThemeVersion -}} + {{- $DesignedBy := `Jimmy` -}} + + {{ T "footer.builtWith" (dict "Generator" $Generator) | safeHTML }}
+ {{ T "footer.designedBy" (dict "Theme" $Theme "DesignedBy" $DesignedBy) | safeHTML }} +
+
diff --git a/themes/stack/layouts/partials/footer/include.html b/themes/stack/layouts/partials/footer/include.html new file mode 100644 index 0000000..54d85a8 --- /dev/null +++ b/themes/stack/layouts/partials/footer/include.html @@ -0,0 +1,3 @@ +{{ partial "cookies/include.html" . }} +{{ partialCached "footer/components/script.html" . }} +{{ partial "footer/custom.html" . }} diff --git a/themes/stack/layouts/partials/head/colorScheme.html b/themes/stack/layouts/partials/head/colorScheme.html new file mode 100644 index 0000000..04d63a5 --- /dev/null +++ b/themes/stack/layouts/partials/head/colorScheme.html @@ -0,0 +1,39 @@ +{{- $defaultColorScheme := .Site.Params.colorScheme.default -}} +{{- if not .Site.Params.colorScheme.toggle -}} + {{/* If toggle is disabled, force default scheme */}} + +{{- else -}} + {{/* Otherwise set to default scheme only if no preference is set by user */}} + +{{- end -}} + + diff --git a/themes/stack/layouts/partials/head/custom-font.html b/themes/stack/layouts/partials/head/custom-font.html new file mode 100644 index 0000000..8ab9cb2 --- /dev/null +++ b/themes/stack/layouts/partials/head/custom-font.html @@ -0,0 +1,4 @@ + + + + diff --git a/themes/stack/layouts/partials/head/custom.html b/themes/stack/layouts/partials/head/custom.html new file mode 100644 index 0000000..e69de29 diff --git a/themes/stack/layouts/partials/head/head.html b/themes/stack/layouts/partials/head/head.html new file mode 100644 index 0000000..00f31d5 --- /dev/null +++ b/themes/stack/layouts/partials/head/head.html @@ -0,0 +1,37 @@ + + + +{{- $description := partialCached "data/description" . .RelPermalink -}} + +{{ with .Params.Keywords }}{{ end }} + +{{- $title := partial "data/title" . -}} +{{ $title }} + +{{ with .Params.canonicalUrl }} + +{{ else }} + +{{ end }} + +{{- partial "head/custom-font.html" . -}} +{{- partial "head/style.html" . -}} +{{- partial "head/script.html" . -}} +{{- partial "head/opengraph/include.html" . -}} + +{{- range .AlternativeOutputFormats -}} + +{{- end -}} + +{{- $favicon := partial "helper/image" (dict "Image" .Site.Params.favicon "Resources" resources "Context" .) -}} +{{ with $favicon }} + +{{ end }} + +{{- if .Site.Params.cookies.enabled -}} + {{- partial "cookies/analytics.html" . -}} +{{- else -}} + {{- template "_internal/google_analytics.html" . -}} +{{- end -}} + +{{- partial "head/custom.html" . -}} diff --git a/themes/stack/layouts/partials/head/opengraph/include.html b/themes/stack/layouts/partials/head/opengraph/include.html new file mode 100644 index 0000000..3a43f48 --- /dev/null +++ b/themes/stack/layouts/partials/head/opengraph/include.html @@ -0,0 +1,2 @@ +{{ partial "head/opengraph/provider/base" . }} +{{ partial "head/opengraph/provider/twitter" . }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/head/opengraph/provider/base.html b/themes/stack/layouts/partials/head/opengraph/provider/base.html new file mode 100644 index 0000000..9fa2ab0 --- /dev/null +++ b/themes/stack/layouts/partials/head/opengraph/provider/base.html @@ -0,0 +1,43 @@ +{{- $title := partialCached "data/title" . .RelPermalink -}} +{{- $description := partialCached "data/description" . .RelPermalink -}} + + + + + + + +{{- with .Params.locale -}} + +{{- end -}} + +{{- if .IsPage -}} + + {{- range .Params.tags -}} + + {{- end -}} +{{- end -}} + +{{- if .IsPage -}} + {{- if not .Date.IsZero -}} + + {{- end -}} + {{- if not .Lastmod.IsZero -}} + + {{- end -}} +{{- else -}} + {{- if not .Site.Lastmod.IsZero -}} + + {{- end -}} +{{- end -}} + +{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }} +{{- if $image -}} + +{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/partials/head/opengraph/provider/twitter.html b/themes/stack/layouts/partials/head/opengraph/provider/twitter.html new file mode 100644 index 0000000..111d266 --- /dev/null +++ b/themes/stack/layouts/partials/head/opengraph/provider/twitter.html @@ -0,0 +1,16 @@ +{{- with .Site.Params.opengraph.twitter.site -}} + + +{{- end -}} + +{{- $title := partialCached "data/title" . .RelPermalink -}} +{{- $description := partialCached "data/description" . .RelPermalink -}} + + + + +{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}} +{{- if $image -}} + + +{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/partials/head/script.html b/themes/stack/layouts/partials/head/script.html new file mode 100644 index 0000000..e69de29 diff --git a/themes/stack/layouts/partials/head/style.html b/themes/stack/layouts/partials/head/style.html new file mode 100644 index 0000000..30ca26e --- /dev/null +++ b/themes/stack/layouts/partials/head/style.html @@ -0,0 +1,3 @@ +{{ $sass := resources.Get "scss/style.scss" }} +{{ $style := $sass | toCSS | minify | resources.Fingerprint "sha256" }} + \ No newline at end of file diff --git a/themes/stack/layouts/partials/helper/color-from-str.html b/themes/stack/layouts/partials/helper/color-from-str.html new file mode 100644 index 0000000..6fc00cb --- /dev/null +++ b/themes/stack/layouts/partials/helper/color-from-str.html @@ -0,0 +1,13 @@ +{{/* + This helper is used to generate a background color and text color from a string + Used to generate deterministic colors for tags, categories, etc. +*/}} + +{{- $str := . -}} +{{- $hash := hash.FNV32a $str -}} +{{- $h := mod $hash 360 -}} + +{{- $backgroundColor := printf "hsl(%d, 60%%, 84%%)" $h -}} +{{- $textColor := printf "hsl(%d, 60%%, 15%%)" $h -}} + +{{- return dict "BackgroundColor" $backgroundColor "TextColor" $textColor "Hue" $h -}} diff --git a/themes/stack/layouts/partials/helper/external.html b/themes/stack/layouts/partials/helper/external.html new file mode 100644 index 0000000..cd0e1fd --- /dev/null +++ b/themes/stack/layouts/partials/helper/external.html @@ -0,0 +1,29 @@ +{{- $List := index hugo.Data.external .Namespace -}} +{{- with $List -}} + {{- range . -}} + {{- if eq .type "script" -}} + + {{- else if eq .type "style" -}} + + {{- else -}} + {{- errorf "Error: unknown external resource type: %s" .type -}} + {{- end -}} + {{- end -}} +{{- else -}} + {{- errorf "Error: external resource '%s' is not found" .Namespace -}} +{{- end -}} diff --git a/themes/stack/layouts/partials/helper/icon.html b/themes/stack/layouts/partials/helper/icon.html new file mode 100644 index 0000000..72162e8 --- /dev/null +++ b/themes/stack/layouts/partials/helper/icon.html @@ -0,0 +1,6 @@ +{{- $iconFile := resources.GetMatch (printf "icons/%s.svg" .) -}} +{{- if $iconFile -}} + {{- $iconFile.Content | safeHTML -}} +{{- else -}} + {{- errorf "Error: icon '%s.svg' is not found under 'assets/icons' folder" . -}} +{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/partials/helper/image.html b/themes/stack/layouts/partials/helper/image.html new file mode 100644 index 0000000..51e71ff --- /dev/null +++ b/themes/stack/layouts/partials/helper/image.html @@ -0,0 +1,72 @@ +/// Params: +/// Resources: Where to search for images +/// Image: Image URL +/// Context: page context (used to access the configuration) + +/// Returns: +/// Resource: Hugo image resource object +/// Permalink: Image URL +/// Height: Image height +/// Width: Image width + +{{ $result := dict }} + +{{ if .Image }} + {{ $url := urls.Parse .Image }} + {{ $permalink := .Image }} + + {{ $result = dict + "Resource" nil + "Permalink" $permalink + "Height" nil + "Width" nil + }} + {{ $resource := dict }} + {{ $local := true }} + + {{ if not (in (slice "https" "http") $url.Scheme) }} + {{/* Local image */}} + {{ $resource = .Resources.Get (printf "%s" (.Image | safeURL)) }} + {{ else }} + {{/* Remote image */}} + {{ with try (resources.GetRemote $url (dict "timeout" .Context.Site.Params.imageProcessing.external.timeout)) }} + {{ with .Err }} + {{ warnf "Failed to fetch remote resource %q: %s" $url . }} + {{ else with .Value }} + {{ $resource = . }} + {{ $local = false }} + {{ else }} + {{ warnf "Unable to get remote resource %q" $url }} + {{ end }} + {{ end }} + {{ end }} + + {{ if $resource }} + {{ if and .Context.Site.Params.imageProcessing.autoOrient (reflect.IsImageResourceProcessable $resource) }} + {{ $filter := images.AutoOrient }} + {{ $resource = $resource | images.Filter $filter }} + {{ end }} + + {{ if $local }} + {{ $permalink = $resource.RelPermalink }} + {{ end }} + + {{ if reflect.IsImageResourceWithMeta $resource }} + {{ $result = dict + "Resource" $resource + "Permalink" $permalink + "Height" $resource.Height + "Width" $resource.Width + }} + {{ else }} + {{ $result = dict + "Resource" $resource + "Permalink" $permalink + "Height" nil + "Width" nil + }} + {{ end }} + {{ end }} +{{ end }} + +{{ return $result }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/helper/pages-sort.html b/themes/stack/layouts/partials/helper/pages-sort.html new file mode 100644 index 0000000..9f767c6 --- /dev/null +++ b/themes/stack/layouts/partials/helper/pages-sort.html @@ -0,0 +1,7 @@ +{{- $pages := .Pages -}} + +{{- if eq .Context.Site.Params.SortBy "lastmod" -}} + {{- $pages = $pages.ByLastmod.Reverse -}} +{{- end -}} + +{{- return $pages -}} \ No newline at end of file diff --git a/themes/stack/layouts/partials/helper/pages.html b/themes/stack/layouts/partials/helper/pages.html new file mode 100644 index 0000000..bca070a --- /dev/null +++ b/themes/stack/layouts/partials/helper/pages.html @@ -0,0 +1,16 @@ +{{- $pages := .Pages -}} + +{{- if .IsHome -}} + {{- $pages = where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}} +{{- else if or (eq .Kind "section") (eq .Kind "taxonomy") (eq .Kind "term") -}} + {{- $subsections := .Sections -}} + {{- $pages = .Pages | complement $subsections -}} + + {{- if eq (len $pages) 0 -}} + {{- $pages = $subsections -}} + {{- end -}} +{{- end -}} + +{{- $pages := partial "helper/pages-sort.html" (dict "Pages" $pages "Context" .) -}} + +{{- return $pages -}} diff --git a/themes/stack/layouts/partials/helper/paginator.html b/themes/stack/layouts/partials/helper/paginator.html new file mode 100644 index 0000000..bba32a3 --- /dev/null +++ b/themes/stack/layouts/partials/helper/paginator.html @@ -0,0 +1,8 @@ +{{- $result := slice -}} + +{{- if or .IsHome (eq .Kind "section") (eq .Kind "taxonomy") (eq .Kind "term") -}} + {{- $pages := partial "helper/pages.html" . -}} + {{- $result = .Paginate $pages -}} +{{- end -}} + +{{- return $result -}} diff --git a/themes/stack/layouts/partials/helper/responsive-image.html b/themes/stack/layouts/partials/helper/responsive-image.html new file mode 100644 index 0000000..63cc2de --- /dev/null +++ b/themes/stack/layouts/partials/helper/responsive-image.html @@ -0,0 +1,33 @@ +{{- /* +Params: + Resource: Hugo image resource object (Optional, for processing) + Widths: Slice of widths to generate for srcset + Attributes: Map of HTML attributes for the tag +*/ -}} + +{{- $resource := .Resource -}} +{{- $widths := .Widths -}} +{{- $attributes := .Attributes | default dict -}} + +{{/* Generate srcset if Resource and Widths are provided */}} +{{- if and $widths $resource (reflect.IsImageResourceProcessable $resource) -}} + {{- $srcset := slice -}} + {{- range $widths -}} + {{- if lt . $resource.Width -}} + {{- $resized := $resource.Resize (printf "%dx" .) -}} + {{- $srcset = $srcset | append (printf "%s %dw" $resized.RelPermalink .) -}} + {{- end -}} + {{- end -}} + + {{- if gt (len $srcset) 0 -}} + {{- $permalink := default $resource.RelPermalink (index $attributes "src") -}} + {{- $srcset = $srcset | append (printf "%s %dw" $permalink $resource.Width) -}} + {{- $attributes = merge $attributes (dict "srcset" (delimit $srcset ", ")) -}} + {{- end -}} +{{- end -}} + + \ No newline at end of file diff --git a/themes/stack/layouts/partials/helper/thumbnail-image.html b/themes/stack/layouts/partials/helper/thumbnail-image.html new file mode 100644 index 0000000..442371b --- /dev/null +++ b/themes/stack/layouts/partials/helper/thumbnail-image.html @@ -0,0 +1,41 @@ +{{- /* +Params: + Resource: Hugo image resource object (Optional, for processing) + Width: Image width (Required) + Height: Image height (Required) + Resize: Whether to perform resize (Optional, default: false) + Attributes: Map of HTML attributes for the tag +*/ -}} + +{{- $resource := .Resource -}} +{{- $width := .Width -}} +{{- $height := .Height -}} +{{- $resize := .Resize -}} +{{- $attributes := .Attributes | default dict -}} + +{{- if and $resize $resource (reflect.IsImageResourceProcessable $resource) $width $height -}} + {{/* Create thumbnail with 1x/2x descriptors */}} + {{- $srcset := slice -}} + {{- range (slice 1 2) -}} + {{- $w := mul $width . -}} + {{- $h := mul $height . -}} + {{- if and (le $w $resource.Width) (le $h $resource.Height) -}} + {{- $resized := $resource.Fill (printf "%dx%d" $w $h) -}} + {{- $srcset = $srcset | append (printf "%s %dx" $resized.RelPermalink .) -}} + + {{- if eq . 1 -}} + {{- $attributes = merge $attributes (dict "src" $resized.RelPermalink) -}} + {{- end -}} + {{- end -}} + {{- end -}} + + {{- if gt (len $srcset) 0 -}} + {{- $attributes = merge $attributes (dict "width" $width "height" $height "srcset" (delimit $srcset ", ")) -}} + {{- end -}} +{{- end -}} + + diff --git a/themes/stack/layouts/partials/pagination.html b/themes/stack/layouts/partials/pagination.html new file mode 100644 index 0000000..abfb64a --- /dev/null +++ b/themes/stack/layouts/partials/pagination.html @@ -0,0 +1,142 @@ +{{- $pag := $.Paginator -}} +{{- if gt $pag.TotalPages 1 -}} + {{- /* Find page 2's URL to extract the pagination base path for JavaScript */ -}} + {{- $page2Url := "" -}} + {{- range $pag.Pagers -}} + {{- if eq .PageNumber 2 -}} + {{- $page2Url = .URL -}} + {{- end -}} + {{- end -}} + + + + +
+
+ + (1 - {{ $pag.TotalPages }}) +
+
+ + +
+
+ Enter + {{ i18n "pagination.pressEnter" | default "Press Enter to jump" }} +
+
+
+{{- end -}} diff --git a/themes/stack/layouts/partials/sidebar/left.html b/themes/stack/layouts/partials/sidebar/left.html new file mode 100644 index 0000000..46f0647 --- /dev/null +++ b/themes/stack/layouts/partials/sidebar/left.html @@ -0,0 +1,95 @@ + diff --git a/themes/stack/layouts/partials/sidebar/right.html b/themes/stack/layouts/partials/sidebar/right.html new file mode 100644 index 0000000..c27e40b --- /dev/null +++ b/themes/stack/layouts/partials/sidebar/right.html @@ -0,0 +1,16 @@ +{{- $scope := default "homepage" .Scope -}} +{{- $context := .Context -}} +{{- with (index .Context.Site.Params.widgets $scope) -}} + +{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/widget/archives.html b/themes/stack/layouts/partials/widget/archives.html new file mode 100644 index 0000000..c2ea933 --- /dev/null +++ b/themes/stack/layouts/partials/widget/archives.html @@ -0,0 +1,37 @@ +{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "archives") -}} +{{- $context := .Context -}} +{{- $limit := default 5 .Params.limit -}} +{{- if $query -}} + {{- $archivesPage := index $query 0 -}} +
+
+ {{ partial "helper/icon" "infinity" }} +
+

+ + {{ T "widget.archives.title" }} + +

+ + {{ $pages := partial "helper/pages.html" $context.Site.Home }} + {{ $archives := $pages.GroupByDate "2006" }} + +
+ {{ range $index, $item := first (add $limit 1) ($archives) }} + {{- $id := lower (replace $item.Key " " "-") -}} + + {{ end }} +
+
+{{- else -}} + {{- warnf "Archives page not found. Create a page with layout: archives." -}} +{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/partials/widget/categories.html b/themes/stack/layouts/partials/widget/categories.html new file mode 100644 index 0000000..7179b77 --- /dev/null +++ b/themes/stack/layouts/partials/widget/categories.html @@ -0,0 +1,2 @@ +{{ $params := merge (dict "taxonomy" "categories" "title" (T "widget.categoriesCloud.title") "icon" "categories") .Params }} +{{ partial "widget/taxonomy" (dict "Context" .Context "Params" $params) }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/widget/search.html b/themes/stack/layouts/partials/widget/search.html new file mode 100644 index 0000000..010ddbe --- /dev/null +++ b/themes/stack/layouts/partials/widget/search.html @@ -0,0 +1,16 @@ +{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "search") -}} +{{- if $query -}} + {{- $searchPage := index $query 0 -}} +
+

+ + + + +

+
+{{- else -}} + {{- warnf "Search page not found. Create a page with layout: search." -}} +{{- end -}} \ No newline at end of file diff --git a/themes/stack/layouts/partials/widget/tag-cloud.html b/themes/stack/layouts/partials/widget/tag-cloud.html new file mode 100644 index 0000000..88c3e46 --- /dev/null +++ b/themes/stack/layouts/partials/widget/tag-cloud.html @@ -0,0 +1,2 @@ +{{ $params := merge (dict "taxonomy" "tags" "title" (T "widget.tagCloud.title") "icon" "tag") .Params }} +{{ partial "widget/taxonomy" (dict "Context" .Context "Params" $params) }} \ No newline at end of file diff --git a/themes/stack/layouts/partials/widget/taxonomy.html b/themes/stack/layouts/partials/widget/taxonomy.html new file mode 100644 index 0000000..c6fca84 --- /dev/null +++ b/themes/stack/layouts/partials/widget/taxonomy.html @@ -0,0 +1,39 @@ +{{- $context := .Context -}} + +{{- if not .Params.taxonomy -}} + {{- warnf "Taxonomy field is required" -}} +{{- end -}} + +{{- $taxonomy := index $context.Site.Taxonomies .Params.taxonomy -}} +{{- if not $taxonomy -}} + {{- warnf "Taxonomy %s not found" .Params.taxonomy -}} +{{- end -}} + +{{- $taxonomyPage := $taxonomy.Page -}} +{{- $widgetTitle := .Params.title -}} +{{- $limit := default 10 .Params.limit -}} +{{- $icon := default .Params.taxonomy .Params.icon -}} +{{- $showLink := default true .Params.showLink -}} + +
+
+ {{ partial "helper/icon" $icon }} +
+

+ {{ if and $showLink $taxonomyPage }} + + {{ $widgetTitle }} + + {{ else }} + {{ $widgetTitle }} + {{ end }} +

+ +
+ {{ range first $limit $taxonomy.ByCount }} + + {{ .Page.Title }} + + {{ end }} +
+
diff --git a/themes/stack/layouts/partials/widget/toc.html b/themes/stack/layouts/partials/widget/toc.html new file mode 100644 index 0000000..22efe3e --- /dev/null +++ b/themes/stack/layouts/partials/widget/toc.html @@ -0,0 +1,14 @@ +{{ if (.Context.Scratch.Get "TOCEnabled") }} +
+
+ {{ partial "helper/icon" "hash" }} +
+

{{ T "article.tableOfContents" }}

+ +
+ +
+
+{{ end }} \ No newline at end of file diff --git a/themes/stack/layouts/rss.xml b/themes/stack/layouts/rss.xml new file mode 100644 index 0000000..ba95af7 --- /dev/null +++ b/themes/stack/layouts/rss.xml @@ -0,0 +1,63 @@ +{{- $authorEmail := "" -}} +{{- with site.Params.author -}} + {{- if reflect.IsMap . -}} + {{- with .email -}} + {{- $authorEmail = . -}} + {{- end -}} + {{- end -}} +{{- end }} + +{{- $authorName := "" -}} +{{- with site.Params.author -}} + {{- if reflect.IsMap . -}} + {{- with .name -}} + {{- $authorName = . -}} + {{- end -}} + {{- else -}} + {{- $authorName = . -}} + {{- end -}} +{{- end -}} + +{{- $pages := partial "helper/pages.html" . -}} +{{- $limit := .Site.Config.Services.RSS.Limit -}} +{{- if ge $limit 1 -}} + {{- $pages = $pages | first $limit -}} +{{- end -}} +{{- printf "" | safeHTML -}} + + + {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{ . }} on {{ end }}{{ .Site.Title }}{{ end }} + {{ .Permalink }} + Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{ . }} {{ end }}{{ end }}on {{ .Site.Title }} + Hugo -- gohugo.io + {{ site.Language.Lang }}{{ with $authorEmail }} + {{.}}{{ with $authorName }} ({{ . }}){{ end }}{{ end }}{{ with $authorEmail }} + {{ . }}{{ with $authorName }} ({{ . }}){{ end }}{{ end }}{{ with .Site.Copyright }} + {{ . }}{{ end }}{{ if not .Date.IsZero }} + {{ (index $pages.ByLastmod.Reverse 0).Lastmod.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} + {{- with .OutputFormats.Get "RSS" -}} + {{ printf "" .Permalink .MediaType | safeHTML }} + {{- end -}} + {{- range $pages -}} + + {{ .Title }} + {{ .Permalink }} + {{ .PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} + {{- with $authorEmail }}{{ . }}{{ with $authorName }} ({{ . }}){{ end }}{{ end }} + {{ .Permalink }} + + {{- $content := .Summary -}} + {{- if .Site.Params.RSSFullContent -}} + {{- $content = .Content -}} + {{- end -}} + {{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}} + {{- if $image -}} + {{- $imgTag := printf "\"Featured" ($image.Permalink | absURL) .Title -}} + {{- $content = printf "%s%s" $imgTag $content -}} + {{- end -}} + {{- $content | transform.XMLEscape | safeHTML -}} + + + {{- end -}} + + diff --git a/themes/stack/layouts/single.html b/themes/stack/layouts/single.html new file mode 100644 index 0000000..5965572 --- /dev/null +++ b/themes/stack/layouts/single.html @@ -0,0 +1,46 @@ +{{ define "body-class" }} + article-page + {{/* + Enable the right sidebar if + - Widget different from 'TOC' is enabled + - TOC is enabled and not empty + */}} + {{- $HasWidgetNotTOC := false -}} + {{- $TOCWidgetEnabled := false -}} + {{- range .Site.Params.widgets.page -}} + {{- if ne .type "toc" -}} + {{ $HasWidgetNotTOC = true -}} + {{- else -}} + {{ $TOCWidgetEnabled = true -}} + {{- end -}} + {{- end -}} + + {{- $TOCEnabled := and (default .Site.Params.article.toc .Params.toc) $TOCWidgetEnabled -}} + {{- $hasTOC := ge (len .TableOfContents) 100 -}} + {{- .Scratch.Set "TOCEnabled" (and $TOCEnabled $hasTOC) -}} + + {{- .Scratch.Set "hasWidget" (or $HasWidgetNotTOC (and $TOCEnabled $hasTOC)) -}} +{{ end }} + +{{ define "main" }} + {{ partial "article/article.html" . }} + + {{ if .Params.links }} + {{ partial "article/components/links" . }} + {{ end }} + + {{ partial "article/components/related-content" . }} + + {{ if not (eq .Params.comments false) }} + {{ partial "comments/include" . }} + {{ end }} + + {{ partialCached "footer/footer" . }} + + {{ partialCached "article/components/photoswipe" . }} + {{ partial "article/components/mermaid" . }} +{{ end }} + +{{ define "right-sidebar" }} + {{ if .Scratch.Get "hasWidget" }}{{ partial "sidebar/right.html" (dict "Context" . "Scope" "page") }}{{ end}} +{{ end }} diff --git a/themes/stack/theme.toml b/themes/stack/theme.toml new file mode 100644 index 0000000..f926a59 --- /dev/null +++ b/themes/stack/theme.toml @@ -0,0 +1,27 @@ +# theme.toml template for a Hugo theme +# See https://github.com/gohugoio/hugoThemes#themetoml for an example + +name = "Stack" +license = "GPL-3.0-only" +licenselink = "https://github.com/CaiJimmy/hugo-theme-stack/blob/master/LICENSE" +description = "Card-style Hugo theme designed for bloggers" +homepage = "https://github.com/CaiJimmy/hugo-theme-stack" +demosite = "https://demo.stack.jimmycai.com" + +tags = ["blog", "responsive", "clean", "light", "dark", "personal"] + +features = [ + "disqus", + "photoswipe", + "opengraph", + "widgets", + "darkmode", + "table of contents", + "search", +] + +min_version = "0.157.0" + +[author] + name = "Jimmy Cai" + homepage = "https://jimmycai.com" diff --git a/themes/stack/wrangler.toml b/themes/stack/wrangler.toml new file mode 100644 index 0000000..341568a --- /dev/null +++ b/themes/stack/wrangler.toml @@ -0,0 +1,9 @@ +name = "hugo-theme-stack" +compatibility_date = "2026-01-25" + +[build] + command = "chmod a+x ./.ci/build.sh && ./.ci/build.sh" + +[assets] + directory = "demo/public" + not_found_handling = "404-page"