├── .github
├── FUNDING.yml
└── workflows
│ └── publish.yml
├── src
├── tailwind.css
├── handleClosePopup.ts
├── index.html
├── index.tsx
├── RecurCard.tsx
├── Recurrence.tsx
└── App.css
├── screenshots
├── demo.gif
└── demo2.gif
├── .postcssrc
├── tailwind.config.js
├── icon.svg
├── README.md
├── LICENSE.md
├── package.json
└── .gitignore
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [hkgnp]
2 |
--------------------------------------------------------------------------------
/src/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/screenshots/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjypng/logseq-recurrence-plugin/HEAD/screenshots/demo.gif
--------------------------------------------------------------------------------
/screenshots/demo2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benjypng/logseq-recurrence-plugin/HEAD/screenshots/demo2.gif
--------------------------------------------------------------------------------
/.postcssrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "postcss-import": {},
4 | "tailwindcss/nesting": {},
5 | "tailwindcss": {},
6 | "autoprefixer": {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/handleClosePopup.ts:
--------------------------------------------------------------------------------
1 | export const handleClosePopup = () => {
2 | //ESC
3 | document.addEventListener(
4 | 'keydown',
5 | function (e) {
6 | if (e.keyCode === 27) {
7 | logseq.hideMainUI({ restoreEditingCursor: true });
8 | }
9 | e.stopPropagation();
10 | },
11 | false
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./src/**/*.{vue,js,ts,jsx,tsx,hbs,html}'],
3 | darkMode: 'media', // or 'media' or 'class'
4 | theme: {
5 | extend: {
6 | spacing: {
7 | 100: '50rem',
8 | },
9 | },
10 | },
11 | variants: {
12 | extend: {},
13 | },
14 | plugins: [],
15 | };
16 |
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [[:gift_heart: Sponsor this project on Github](https://github.com/sponsors/hkgnp) or [:coffee: Get me a coffee](https://www.buymeacoffee.com/hkgnp.dev) if you like this plugin!
2 |
3 | # Overview
4 |
5 | This plugin allows you to quickly add recurring blocks based on your desired recurrence. It also allows you to delete inserted recurring blocks as well!
6 |
7 | When used with the [logseq-trackhabits2-plugin](https://github.com/hkgnp/logseq-trackhabits2-plugin), you can easily create habits to track!
8 |
9 | 
10 |
11 | 
12 |
13 | # Installation
14 |
15 | If not in the marketplace, [download the release](https://github.com/hkgnp/logseq-recurrence-plugin/releases) and manually load it in Logseq.
16 |
17 | # Usage
18 |
19 | 1. Create the block that you would want to recur.
20 | 2. At the end of the line, type `/Set recurrence`.
21 | 3. Choose the desired recurrence options.
22 | 4. Click `Create Blocks` and you are done!
23 |
24 | # Credits
25 |
26 | - [DayJS](https://day.js.org/)
27 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 hkgnp
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import '@logseq/libs';
2 | import './App.css';
3 | import { handleClosePopup } from './handleClosePopup';
4 | import React from 'react';
5 | import ReactDOM from 'react-dom';
6 | import Recurrence from './Recurrence';
7 |
8 | const main = () => {
9 | console.log('logseq-recurrence-plugin loaded');
10 |
11 | window.setTimeout(async () => {
12 | const userConfigs = await logseq.App.getUserConfigs();
13 | const preferredDateFormat: string = userConfigs.preferredDateFormat;
14 | logseq.updateSettings({ preferredDateFormat: preferredDateFormat });
15 | console.log(`Settings updated to ${preferredDateFormat}`);
16 | }, 3000);
17 |
18 | if (!logseq.settings.recurrences) {
19 | logseq.updateSettings({
20 | recurrences: [],
21 | });
22 | }
23 |
24 | logseq.Editor.registerSlashCommand('Set Recurrence', async () => {
25 | ReactDOM.render(
26 |
27 |
28 | ,
29 | document.getElementById('app')
30 | );
31 |
32 | logseq.showMainUI();
33 | });
34 |
35 | handleClosePopup();
36 | };
37 |
38 | logseq.ready(main).catch(console.error);
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "logseq": {
3 | "id": "logseq-recurrence-plugin",
4 | "title": "logseq-recurrence-plugin",
5 | "icon": "./icon.svg"
6 | },
7 | "name": "logseq-recurrence-plugin",
8 | "version": "1.0.7",
9 | "description": "",
10 | "main": "dist/index.html",
11 | "targets": {
12 | "main": false
13 | },
14 | "scripts": {
15 | "test": "echo \"Error: no test specified\" && exit 1",
16 | "build": "postcss src/tailwind.css -o src/App.css && parcel build --no-source-maps src/index.html --public-url ./"
17 | },
18 | "keywords": [],
19 | "author": "hkgnp",
20 | "license": "MIT",
21 | "dependencies": {
22 | "@logseq/libs": "^0.0.1-alpha.34",
23 | "autoprefix": "^1.0.1",
24 | "dayjs": "^1.10.7",
25 | "logseq-dateutils": "^0.0.12",
26 | "postcss-cli": "^9.1.0",
27 | "postcss-import": "^14.0.2",
28 | "react": "^17.0.2",
29 | "react-datepicker": "^4.6.0",
30 | "react-dom": "^17.0.2",
31 | "tailwindcss": "^3.0.15"
32 | },
33 | "devDependencies": {
34 | "@types/react": "^17.0.38",
35 | "@types/react-datepicker": "^4.4.0",
36 | "@types/react-dom": "^17.0.11",
37 | "parcel": "^2.2.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/RecurCard.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const RecurCard = (props: { uuids: any[]; id: string; item: string }) => {
4 | const [uuids] = useState(props.uuids);
5 | const [settings, setSettings] = useState(logseq.settings);
6 |
7 | const deleteBlocks = async () => {
8 | // delete all blocks
9 | for (let u of uuids) {
10 | await logseq.Editor.removeBlock(u);
11 | }
12 |
13 | // delete entry from settings by matching the id from props and the dateAdded from settings
14 | const recurrencesClone: any[] = settings.recurrences;
15 | recurrencesClone.splice(
16 | recurrencesClone.findIndex((i) => i.dateAdded === props.id),
17 | 1
18 | );
19 | setSettings((prevSettings) => ({
20 | ...prevSettings,
21 | recurrences: recurrencesClone,
22 | }));
23 | logseq.hideMainUI();
24 | logseq.App.showMsg("Blocks deleted successfully!");
25 | };
26 |
27 | return (
28 |
29 |
32 |
33 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default RecurCard;
46 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Build plugin
2 |
3 | on:
4 | push:
5 | # Sequence of patterns matched against refs/tags
6 | tags:
7 | - '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
8 |
9 | env:
10 | PLUGIN_NAME: ${{ github.event.repository.name }}
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Use Node.js
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: '16.x' # You might need to adjust this value to your own version
22 | - name: Build
23 | id: build
24 | run: |
25 | npm i && npm run build
26 | mkdir ${{ env.PLUGIN_NAME }}
27 | cp README.md package.json icon.svg ${{ env.PLUGIN_NAME }}
28 | mv dist ${{ env.PLUGIN_NAME }}
29 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
30 | ls
31 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
32 | - name: Create Release
33 | uses: ncipollo/release-action@v1
34 | id: create_release
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 | VERSION: ${{ github.ref }}
38 | with:
39 | allowUpdates: true
40 | draft: false
41 | prerelease: false
42 |
43 | - name: Upload zip file
44 | id: upload_zip
45 | uses: actions/upload-release-asset@v1
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | with:
49 | upload_url: ${{ steps.create_release.outputs.upload_url }}
50 | asset_path: ./${{ env.PLUGIN_NAME }}.zip
51 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
52 | asset_content_type: application/zip
53 |
54 | - name: Upload package.json
55 | id: upload_metadata
56 | uses: actions/upload-release-asset@v1
57 | env:
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | with:
60 | upload_url: ${{ steps.create_release.outputs.upload_url }}
61 | asset_path: ./package.json
62 | asset_name: package.json
63 | asset_content_type: application/json
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | # Logs
3 | logs
4 | *.log
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 | *.lcov
26 |
27 | # nyc test coverage
28 | .nyc_output
29 |
30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
31 | .grunt
32 |
33 | # Bower dependency directory (https://bower.io/)
34 | bower_components
35 |
36 | # node-waf configuration
37 | .lock-wscript
38 |
39 | # Compiled binary addons (https://nodejs.org/api/addons.html)
40 | build/Release
41 |
42 | # Dependency directories
43 | node_modules/
44 | jspm_packages/
45 |
46 | # Snowpack dependency directory (https://snowpack.dev/)
47 | web_modules/
48 |
49 | # TypeScript cache
50 | *.tsbuildinfo
51 |
52 | # Optional npm cache directory
53 | .npm
54 |
55 | # Optional eslint cache
56 | .eslintcache
57 |
58 | # Microbundle cache
59 | .rpt2_cache/
60 | .rts2_cache_cjs/
61 | .rts2_cache_es/
62 | .rts2_cache_umd/
63 |
64 | # Optional REPL history
65 | .node_repl_history
66 |
67 | # Output of 'npm pack'
68 | *.tgz
69 |
70 | # Yarn Integrity file
71 | .yarn-integrity
72 |
73 | # dotenv environment variables file
74 | .env
75 | .env.test
76 | .env.production
77 |
78 | # parcel-bundler cache (https://parceljs.org/)
79 | .cache
80 | .parcel-cache
81 |
82 | # Next.js build output
83 | .next
84 | out
85 |
86 | # Nuxt.js build / generate output
87 | .nuxt
88 | dist
89 |
90 | # Gatsby files
91 | .cache/
92 | # Comment in the public line in if your project uses Gatsby and not Next.js
93 | # https://nextjs.org/blog/next-9-1#public-directory-support
94 | # public
95 |
96 | # vuepress build output
97 | .vuepress/dist
98 |
99 | # Serverless directories
100 | .serverless/
101 |
102 | # FuseBox cache
103 | .fusebox/
104 |
105 | # DynamoDB Local files
106 | .dynamodb/
107 |
108 | # TernJS port file
109 | .tern-port
110 |
111 | # Stores VSCode versions used for testing VSCode extensions
112 | .vscode-test
113 |
114 | # yarn v2
115 | .yarn/cache
116 | .yarn/unplugged
117 | .yarn/build-state.yml
118 | .yarn/install-state.gz
119 | .pnp.*
--------------------------------------------------------------------------------
/src/Recurrence.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import DatePicker from "react-datepicker";
3 | import "react-datepicker/dist/react-datepicker.css";
4 | import { getDateForPageWithoutBrackets } from "logseq-dateutils";
5 | import dayjs from "dayjs";
6 | import { BlockEntity, PageEntity } from "@logseq/libs/dist/LSPlugin.user";
7 | import RecurCard from "./RecurCard";
8 |
9 | const Recurrence = () => {
10 | const [content, setContent] = useState("");
11 | const [contentUUID, setContentUUID] = useState("");
12 | const [journalDay, setJournalDay] = useState("");
13 |
14 | const getCurrentBlock = async () => {
15 | const currBlock: BlockEntity = await logseq.Editor.getCurrentBlock();
16 | const currPage: PageEntity = await logseq.Editor.getPage(currBlock.page.id);
17 | setJournalDay(currPage.journalDay.toString());
18 | setContent(currBlock.content);
19 | setContentUUID(currBlock.uuid);
20 | };
21 |
22 | useEffect(() => {
23 | getCurrentBlock();
24 | });
25 |
26 | const [recurrenceValues, setRecurrenceValues] = useState({
27 | recurrencePattern: "",
28 | recurrenceType: "",
29 | options: {
30 | endAfter: "",
31 | endBy: null,
32 | },
33 | });
34 |
35 | const handleForm = (e: any) => {
36 | if (!e.type) {
37 | setRecurrenceValues((prevValues) => ({
38 | ...prevValues,
39 | options: {
40 | endAfter: "",
41 | endBy: e,
42 | },
43 | }));
44 | } else {
45 | const name = e.target.name.split(".");
46 | if (name.length > 1) {
47 | setRecurrenceValues((prevValues) => ({
48 | ...prevValues,
49 | [name[0]]: { [name[1]]: e.target.value, endBy: "" },
50 | }));
51 | } else {
52 | setRecurrenceValues((prevValues) => ({
53 | ...prevValues,
54 | [name[0]]: e.target.value,
55 | }));
56 | }
57 | }
58 | };
59 |
60 | const resetForm = () => {
61 | setRecurrenceValues({
62 | recurrencePattern: "",
63 | recurrenceType: "",
64 | options: {
65 | endAfter: "",
66 | endBy: "",
67 | },
68 | });
69 | };
70 |
71 | const createBlocks = async () => {
72 | if (content === "" || !content) {
73 | logseq.App.showMsg("You have no content to recur", "error");
74 | return;
75 | }
76 |
77 | // Get basic settings
78 | const { recurrencePattern, recurrenceType, options } = recurrenceValues;
79 | const { preferredDateFormat } = logseq.settings;
80 |
81 | const d = new Date();
82 | let dates = [];
83 | let settingsToBeSaved = {
84 | item: content,
85 | dateAdded: dayjs(d).unix(),
86 | uuids: [contentUUID],
87 | };
88 |
89 | // Create blocks
90 | if (recurrenceType === "occurrences") {
91 | if (parseInt(options.endAfter) < 1) {
92 | logseq.App.showMsg(
93 | "You have indicated a negative or zero occurence.",
94 | "error"
95 | );
96 | return;
97 | }
98 |
99 | for (let i = 0; i < parseInt(options.endAfter); i++) {
100 | if (recurrencePattern === "daily") {
101 | const payload = getDateForPageWithoutBrackets(
102 | dayjs(journalDay).add(i, "day").toDate(),
103 | preferredDateFormat
104 | );
105 | dates.push(payload.toLowerCase());
106 | } else if (recurrencePattern === "weekly") {
107 | const payload = getDateForPageWithoutBrackets(
108 | dayjs(journalDay).add(i, "week").toDate(),
109 | preferredDateFormat
110 | );
111 | dates.push(payload.toLowerCase());
112 | } else if (recurrencePattern === "monthly") {
113 | const payload = getDateForPageWithoutBrackets(
114 | dayjs(journalDay).add(i, "month").toDate(),
115 | preferredDateFormat
116 | );
117 | dates.push(payload.toLowerCase());
118 | } else if (recurrencePattern === "yearly") {
119 | const payload = getDateForPageWithoutBrackets(
120 | dayjs(journalDay).add(i, "year").toDate(),
121 | preferredDateFormat
122 | );
123 | dates.push(payload.toLowerCase());
124 | }
125 | }
126 | } else if (recurrenceType === "date") {
127 | const pushPayload = (d: Date) => {
128 | const payload = getDateForPageWithoutBrackets(d, preferredDateFormat);
129 | dates.push(payload.toLowerCase());
130 | };
131 | const endByDate = dayjs(new Date(options.endBy)).add(1, "day").toDate();
132 |
133 | let i = 0;
134 | while (true) {
135 | if (recurrencePattern === "daily") {
136 | const d = dayjs(journalDay).add(i, "day").toDate();
137 |
138 | if (d <= endByDate) {
139 | pushPayload(d);
140 | } else {
141 | break;
142 | }
143 | } else if (recurrencePattern === "weekly") {
144 | const d = dayjs(journalDay).add(i, "week").toDate();
145 |
146 | if (d <= endByDate) {
147 | pushPayload(d);
148 | } else {
149 | break;
150 | }
151 | } else if (recurrencePattern === "monthly") {
152 | const d = dayjs(journalDay).add(i, "month").toDate();
153 |
154 | if (d <= endByDate) {
155 | pushPayload(d);
156 | } else {
157 | break;
158 | }
159 | } else if (recurrencePattern === "yearly") {
160 | const d = dayjs(journalDay).add(i, "year").toDate();
161 |
162 | if (d <= endByDate) {
163 | pushPayload(d);
164 | } else {
165 | break;
166 | }
167 | }
168 | i++;
169 | }
170 | }
171 |
172 | // Add blocks to the designated pages
173 | for (let j = 1; j < dates.length; j++) {
174 | const getPage = await logseq.Editor.getPage(dates[j]);
175 |
176 | if (getPage === null) {
177 | await logseq.Editor.createPage(dates[j], "", {
178 | redirect: false,
179 | createFirstBlock: false,
180 | format: "markdown",
181 | });
182 | }
183 |
184 | const itemBlock = await logseq.Editor.insertBlock(dates[j], content, {
185 | isPageBlock: true,
186 | });
187 |
188 | settingsToBeSaved.uuids.push(itemBlock.uuid);
189 | }
190 |
191 | // Clear forms
192 | resetForm();
193 |
194 | logseq.App.showMsg("Blocks added successfully!");
195 |
196 | logseq.hideMainUI();
197 |
198 | // Save recurrences to settings so can delete them
199 | if (
200 | !logseq.settings.recurrences ||
201 | logseq.settings.recurrences.length === 0
202 | ) {
203 | console.log("Updating settings for the first time...");
204 | logseq.updateSettings({ recurrences: [settingsToBeSaved] });
205 | console.log(logseq.settings);
206 | } else {
207 | console.log("Updating settings...");
208 | let existingSettings: any[] = logseq.settings.recurrences;
209 | existingSettings.push(settingsToBeSaved);
210 | logseq.updateSettings({
211 | recurrences: existingSettings,
212 | });
213 | console.log(logseq.settings);
214 | }
215 | };
216 |
217 | return (
218 |
219 |
220 |
221 | {content}
222 |
223 |
224 |
225 |
226 |
229 |
230 |
231 |
243 |
244 |
245 |
246 |
247 |
248 |
251 |
252 |
253 |
263 |
264 |
265 |
266 | {recurrenceValues.recurrenceType === "occurrences" && (
267 |
268 |
269 |
272 |
273 |
274 | {" "}
284 | occurences
285 |
286 |
287 | )}
288 |
289 | {recurrenceValues.recurrenceType === "date" && (
290 |
291 |
292 |
295 |
296 |
297 |
303 |
304 |
305 | )}
306 |
307 |
308 |
309 |
310 |
317 |
324 |
325 |
326 |
327 |
328 | {logseq.settings.recurrences.length > 0 && (
329 |
Saved Recurrences
330 | )}
331 |
332 | {logseq.settings.recurrences &&
333 | logseq.settings.recurrences.map(
334 | (r: { uuids: any[]; item: string; dateAdded: string }) => (
335 |
336 | )
337 | )}
338 |
339 |
340 | );
341 | };
342 |
343 | export default Recurrence;
344 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | /*
2 | ! tailwindcss v3.0.19 | MIT License | https://tailwindcss.com
3 | *//*
4 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
5 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
6 | */
7 |
8 | *,
9 | ::before,
10 | ::after {
11 | box-sizing: border-box; /* 1 */
12 | border-width: 0; /* 2 */
13 | border-style: solid; /* 2 */
14 | border-color: #e5e7eb; /* 2 */
15 | }
16 |
17 | ::before,
18 | ::after {
19 | --tw-content: '';
20 | }
21 |
22 | /*
23 | 1. Use a consistent sensible line-height in all browsers.
24 | 2. Prevent adjustments of font size after orientation changes in iOS.
25 | 3. Use a more readable tab size.
26 | 4. Use the user's configured `sans` font-family by default.
27 | */
28 |
29 | html {
30 | line-height: 1.5; /* 1 */
31 | -webkit-text-size-adjust: 100%; /* 2 */
32 | -moz-tab-size: 4; /* 3 */
33 | -o-tab-size: 4;
34 | tab-size: 4; /* 3 */
35 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */
36 | }
37 |
38 | /*
39 | 1. Remove the margin in all browsers.
40 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
41 | */
42 |
43 | body {
44 | margin: 0; /* 1 */
45 | line-height: inherit; /* 2 */
46 | }
47 |
48 | /*
49 | 1. Add the correct height in Firefox.
50 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
51 | 3. Ensure horizontal rules are visible by default.
52 | */
53 |
54 | hr {
55 | height: 0; /* 1 */
56 | color: inherit; /* 2 */
57 | border-top-width: 1px; /* 3 */
58 | }
59 |
60 | /*
61 | Add the correct text decoration in Chrome, Edge, and Safari.
62 | */
63 |
64 | abbr:where([title]) {
65 | -webkit-text-decoration: underline dotted;
66 | text-decoration: underline dotted;
67 | }
68 |
69 | /*
70 | Remove the default font size and weight for headings.
71 | */
72 |
73 | h1,
74 | h2,
75 | h3,
76 | h4,
77 | h5,
78 | h6 {
79 | font-size: inherit;
80 | font-weight: inherit;
81 | }
82 |
83 | /*
84 | Reset links to optimize for opt-in styling instead of opt-out.
85 | */
86 |
87 | a {
88 | color: inherit;
89 | text-decoration: inherit;
90 | }
91 |
92 | /*
93 | Add the correct font weight in Edge and Safari.
94 | */
95 |
96 | b,
97 | strong {
98 | font-weight: bolder;
99 | }
100 |
101 | /*
102 | 1. Use the user's configured `mono` font family by default.
103 | 2. Correct the odd `em` font sizing in all browsers.
104 | */
105 |
106 | code,
107 | kbd,
108 | samp,
109 | pre {
110 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */
111 | font-size: 1em; /* 2 */
112 | }
113 |
114 | /*
115 | Add the correct font size in all browsers.
116 | */
117 |
118 | small {
119 | font-size: 80%;
120 | }
121 |
122 | /*
123 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
124 | */
125 |
126 | sub,
127 | sup {
128 | font-size: 75%;
129 | line-height: 0;
130 | position: relative;
131 | vertical-align: baseline;
132 | }
133 |
134 | sub {
135 | bottom: -0.25em;
136 | }
137 |
138 | sup {
139 | top: -0.5em;
140 | }
141 |
142 | /*
143 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
144 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
145 | 3. Remove gaps between table borders by default.
146 | */
147 |
148 | table {
149 | text-indent: 0; /* 1 */
150 | border-color: inherit; /* 2 */
151 | border-collapse: collapse; /* 3 */
152 | }
153 |
154 | /*
155 | 1. Change the font styles in all browsers.
156 | 2. Remove the margin in Firefox and Safari.
157 | 3. Remove default padding in all browsers.
158 | */
159 |
160 | button,
161 | input,
162 | optgroup,
163 | select,
164 | textarea {
165 | font-family: inherit; /* 1 */
166 | font-size: 100%; /* 1 */
167 | line-height: inherit; /* 1 */
168 | color: inherit; /* 1 */
169 | margin: 0; /* 2 */
170 | padding: 0; /* 3 */
171 | }
172 |
173 | /*
174 | Remove the inheritance of text transform in Edge and Firefox.
175 | */
176 |
177 | button,
178 | select {
179 | text-transform: none;
180 | }
181 |
182 | /*
183 | 1. Correct the inability to style clickable types in iOS and Safari.
184 | 2. Remove default button styles.
185 | */
186 |
187 | button,
188 | [type='button'],
189 | [type='reset'],
190 | [type='submit'] {
191 | -webkit-appearance: button; /* 1 */
192 | background-color: transparent; /* 2 */
193 | background-image: none; /* 2 */
194 | }
195 |
196 | /*
197 | Use the modern Firefox focus style for all focusable elements.
198 | */
199 |
200 | :-moz-focusring {
201 | outline: auto;
202 | }
203 |
204 | /*
205 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
206 | */
207 |
208 | :-moz-ui-invalid {
209 | box-shadow: none;
210 | }
211 |
212 | /*
213 | Add the correct vertical alignment in Chrome and Firefox.
214 | */
215 |
216 | progress {
217 | vertical-align: baseline;
218 | }
219 |
220 | /*
221 | Correct the cursor style of increment and decrement buttons in Safari.
222 | */
223 |
224 | ::-webkit-inner-spin-button,
225 | ::-webkit-outer-spin-button {
226 | height: auto;
227 | }
228 |
229 | /*
230 | 1. Correct the odd appearance in Chrome and Safari.
231 | 2. Correct the outline style in Safari.
232 | */
233 |
234 | [type='search'] {
235 | -webkit-appearance: textfield; /* 1 */
236 | outline-offset: -2px; /* 2 */
237 | }
238 |
239 | /*
240 | Remove the inner padding in Chrome and Safari on macOS.
241 | */
242 |
243 | ::-webkit-search-decoration {
244 | -webkit-appearance: none;
245 | }
246 |
247 | /*
248 | 1. Correct the inability to style clickable types in iOS and Safari.
249 | 2. Change font properties to `inherit` in Safari.
250 | */
251 |
252 | ::-webkit-file-upload-button {
253 | -webkit-appearance: button; /* 1 */
254 | font: inherit; /* 2 */
255 | }
256 |
257 | /*
258 | Add the correct display in Chrome and Safari.
259 | */
260 |
261 | summary {
262 | display: list-item;
263 | }
264 |
265 | /*
266 | Removes the default spacing and border for appropriate elements.
267 | */
268 |
269 | blockquote,
270 | dl,
271 | dd,
272 | h1,
273 | h2,
274 | h3,
275 | h4,
276 | h5,
277 | h6,
278 | hr,
279 | figure,
280 | p,
281 | pre {
282 | margin: 0;
283 | }
284 |
285 | fieldset {
286 | margin: 0;
287 | padding: 0;
288 | }
289 |
290 | legend {
291 | padding: 0;
292 | }
293 |
294 | ol,
295 | ul,
296 | menu {
297 | list-style: none;
298 | margin: 0;
299 | padding: 0;
300 | }
301 |
302 | /*
303 | Prevent resizing textareas horizontally by default.
304 | */
305 |
306 | textarea {
307 | resize: vertical;
308 | }
309 |
310 | /*
311 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
312 | 2. Set the default placeholder color to the user's configured gray 400 color.
313 | */
314 |
315 | input::-moz-placeholder, textarea::-moz-placeholder {
316 | opacity: 1; /* 1 */
317 | color: #9ca3af; /* 2 */
318 | }
319 |
320 | input:-ms-input-placeholder, textarea:-ms-input-placeholder {
321 | opacity: 1; /* 1 */
322 | color: #9ca3af; /* 2 */
323 | }
324 |
325 | input::placeholder,
326 | textarea::placeholder {
327 | opacity: 1; /* 1 */
328 | color: #9ca3af; /* 2 */
329 | }
330 |
331 | /*
332 | Set the default cursor for buttons.
333 | */
334 |
335 | button,
336 | [role="button"] {
337 | cursor: pointer;
338 | }
339 |
340 | /*
341 | Make sure disabled buttons don't get the pointer cursor.
342 | */
343 | :disabled {
344 | cursor: default;
345 | }
346 |
347 | /*
348 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
349 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
350 | This can trigger a poorly considered lint error in some tools but is included by design.
351 | */
352 |
353 | img,
354 | svg,
355 | video,
356 | canvas,
357 | audio,
358 | iframe,
359 | embed,
360 | object {
361 | display: block; /* 1 */
362 | vertical-align: middle; /* 2 */
363 | }
364 |
365 | /*
366 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
367 | */
368 |
369 | img,
370 | video {
371 | max-width: 100%;
372 | height: auto;
373 | }
374 |
375 | /*
376 | Ensure the default browser behavior of the `hidden` attribute.
377 | */
378 |
379 | [hidden] {
380 | display: none;
381 | }
382 |
383 | *, ::before, ::after {
384 | --tw-translate-x: 0;
385 | --tw-translate-y: 0;
386 | --tw-rotate: 0;
387 | --tw-skew-x: 0;
388 | --tw-skew-y: 0;
389 | --tw-scale-x: 1;
390 | --tw-scale-y: 1;
391 | --tw-pan-x: ;
392 | --tw-pan-y: ;
393 | --tw-pinch-zoom: ;
394 | --tw-scroll-snap-strictness: proximity;
395 | --tw-ordinal: ;
396 | --tw-slashed-zero: ;
397 | --tw-numeric-figure: ;
398 | --tw-numeric-spacing: ;
399 | --tw-numeric-fraction: ;
400 | --tw-ring-inset: ;
401 | --tw-ring-offset-width: 0px;
402 | --tw-ring-offset-color: #fff;
403 | --tw-ring-color: rgb(59 130 246 / 0.5);
404 | --tw-ring-offset-shadow: 0 0 #0000;
405 | --tw-ring-shadow: 0 0 #0000;
406 | --tw-shadow: 0 0 #0000;
407 | --tw-shadow-colored: 0 0 #0000;
408 | --tw-blur: ;
409 | --tw-brightness: ;
410 | --tw-contrast: ;
411 | --tw-grayscale: ;
412 | --tw-hue-rotate: ;
413 | --tw-invert: ;
414 | --tw-saturate: ;
415 | --tw-sepia: ;
416 | --tw-drop-shadow: ;
417 | --tw-backdrop-blur: ;
418 | --tw-backdrop-brightness: ;
419 | --tw-backdrop-contrast: ;
420 | --tw-backdrop-grayscale: ;
421 | --tw-backdrop-hue-rotate: ;
422 | --tw-backdrop-invert: ;
423 | --tw-backdrop-opacity: ;
424 | --tw-backdrop-saturate: ;
425 | --tw-backdrop-sepia: ;
426 | }
427 | .absolute {
428 | position: absolute;
429 | }
430 | .top-10 {
431 | top: 2.5rem;
432 | }
433 | .mb-3 {
434 | margin-bottom: 0.75rem;
435 | }
436 | .mb-6 {
437 | margin-bottom: 1.5rem;
438 | }
439 | .mb-1 {
440 | margin-bottom: 0.25rem;
441 | }
442 | .mb-10 {
443 | margin-bottom: 2.5rem;
444 | }
445 | .mr-2 {
446 | margin-right: 0.5rem;
447 | }
448 | .block {
449 | display: block;
450 | }
451 | .inline {
452 | display: inline;
453 | }
454 | .flex {
455 | display: flex;
456 | }
457 | .w-2\/3 {
458 | width: 66.666667%;
459 | }
460 | .w-full {
461 | width: 100%;
462 | }
463 | .w-1\/3 {
464 | width: 33.333333%;
465 | }
466 | .appearance-none {
467 | -webkit-appearance: none;
468 | -moz-appearance: none;
469 | appearance: none;
470 | }
471 | .flex-row {
472 | flex-direction: row;
473 | }
474 | .items-center {
475 | align-items: center;
476 | }
477 | .justify-center {
478 | justify-content: center;
479 | }
480 | .justify-between {
481 | justify-content: space-between;
482 | }
483 | .rounded {
484 | border-radius: 0.25rem;
485 | }
486 | .rounded-lg {
487 | border-radius: 0.5rem;
488 | }
489 | .border {
490 | border-width: 1px;
491 | }
492 | .border-2 {
493 | border-width: 2px;
494 | }
495 | .border-gray-400 {
496 | --tw-border-opacity: 1;
497 | border-color: rgb(156 163 175 / var(--tw-border-opacity));
498 | }
499 | .border-gray-200 {
500 | --tw-border-opacity: 1;
501 | border-color: rgb(229 231 235 / var(--tw-border-opacity));
502 | }
503 | .bg-red-500 {
504 | --tw-bg-opacity: 1;
505 | background-color: rgb(239 68 68 / var(--tw-bg-opacity));
506 | }
507 | .bg-white {
508 | --tw-bg-opacity: 1;
509 | background-color: rgb(255 255 255 / var(--tw-bg-opacity));
510 | }
511 | .bg-purple-500 {
512 | --tw-bg-opacity: 1;
513 | background-color: rgb(168 85 247 / var(--tw-bg-opacity));
514 | }
515 | .bg-pink-500 {
516 | --tw-bg-opacity: 1;
517 | background-color: rgb(236 72 153 / var(--tw-bg-opacity));
518 | }
519 | .p-3 {
520 | padding: 0.75rem;
521 | }
522 | .py-2 {
523 | padding-top: 0.5rem;
524 | padding-bottom: 0.5rem;
525 | }
526 | .px-4 {
527 | padding-left: 1rem;
528 | padding-right: 1rem;
529 | }
530 | .pr-4 {
531 | padding-right: 1rem;
532 | }
533 | .pr-8 {
534 | padding-right: 2rem;
535 | }
536 | .text-xl {
537 | font-size: 1.25rem;
538 | line-height: 1.75rem;
539 | }
540 | .text-lg {
541 | font-size: 1.125rem;
542 | line-height: 1.75rem;
543 | }
544 | .font-bold {
545 | font-weight: 700;
546 | }
547 | .font-extrabold {
548 | font-weight: 800;
549 | }
550 | .leading-tight {
551 | line-height: 1.25;
552 | }
553 | .text-white {
554 | --tw-text-opacity: 1;
555 | color: rgb(255 255 255 / var(--tw-text-opacity));
556 | }
557 | .text-blue-800 {
558 | --tw-text-opacity: 1;
559 | color: rgb(30 64 175 / var(--tw-text-opacity));
560 | }
561 | .text-gray-500 {
562 | --tw-text-opacity: 1;
563 | color: rgb(107 114 128 / var(--tw-text-opacity));
564 | }
565 | .text-gray-700 {
566 | --tw-text-opacity: 1;
567 | color: rgb(55 65 81 / var(--tw-text-opacity));
568 | }
569 | .underline {
570 | -webkit-text-decoration-line: underline;
571 | text-decoration-line: underline;
572 | }
573 | .shadow {
574 | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
575 | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
576 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
577 | }
578 | .hover\:border-gray-500:hover {
579 | --tw-border-opacity: 1;
580 | border-color: rgb(107 114 128 / var(--tw-border-opacity));
581 | }
582 | .hover\:bg-red-400:hover {
583 | --tw-bg-opacity: 1;
584 | background-color: rgb(248 113 113 / var(--tw-bg-opacity));
585 | }
586 | .hover\:bg-purple-400:hover {
587 | --tw-bg-opacity: 1;
588 | background-color: rgb(192 132 252 / var(--tw-bg-opacity));
589 | }
590 | .hover\:bg-pink-400:hover {
591 | --tw-bg-opacity: 1;
592 | background-color: rgb(244 114 182 / var(--tw-bg-opacity));
593 | }
594 | .focus\:border-purple-500:focus {
595 | --tw-border-opacity: 1;
596 | border-color: rgb(168 85 247 / var(--tw-border-opacity));
597 | }
598 | .focus\:bg-white:focus {
599 | --tw-bg-opacity: 1;
600 | background-color: rgb(255 255 255 / var(--tw-bg-opacity));
601 | }
602 | .focus\:outline-none:focus {
603 | outline: 2px solid transparent;
604 | outline-offset: 2px;
605 | }
606 | @media (min-width: 768px) {
607 |
608 | .md\:mb-0 {
609 | margin-bottom: 0px;
610 | }
611 |
612 | .md\:flex {
613 | display: flex;
614 | }
615 |
616 | .md\:w-1\/6 {
617 | width: 16.666667%;
618 | }
619 |
620 | .md\:w-5\/6 {
621 | width: 83.333333%;
622 | }
623 |
624 | .md\:text-right {
625 | text-align: right;
626 | }
627 | }
628 |
--------------------------------------------------------------------------------