├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── README.md ├── jsconfig.json ├── mdsvex.config.js ├── package.json ├── pnpm-lock.yaml ├── src ├── app.d.ts ├── app.html ├── lib │ ├── .gitignore │ ├── components │ │ ├── Calendar.svelte │ │ ├── SveltyPicker.svelte │ │ └── Time.svelte │ ├── i18n │ │ └── index.js │ ├── index.js │ ├── settings.js │ ├── types │ │ └── internal.d.ts │ └── utils │ │ ├── actions.js │ │ ├── constants.js │ │ ├── custom-element.js │ │ ├── dateUtils.js │ │ ├── grid.js │ │ ├── state.js │ │ └── transitions.js ├── routes │ ├── +layout.server.js │ ├── +layout.svelte │ ├── +page.svelte.md │ ├── autocommit-changes │ │ └── +page.svelte.md │ ├── disabling-dates │ │ └── +page.svelte.md │ ├── global-config │ │ └── +page.svelte.md │ ├── localization │ │ └── +page.svelte.md │ ├── migration-guide │ │ └── +page.svelte.md │ ├── modes-and-formats │ │ ├── +page.svelte.md │ │ ├── Tabs.svelte │ │ ├── format-php.md │ │ └── format-standard.md │ ├── properties │ │ └── +page.svelte.md │ ├── snippets │ │ └── +page.svelte.md │ ├── theme │ │ └── +page.svelte.md │ └── working-with-dates │ │ └── +page.svelte.md ├── style │ ├── base.css │ ├── code.css │ ├── doc.css │ ├── style.css │ └── vars.css └── utils │ └── codeHighlighter.js ├── static └── favicon.png ├── svelte.config.js ├── test ├── 01-render.test.js ├── 02-datepicker.test.svelte.js ├── 03-timepicker.test.svelte.js ├── 04-datetime.test.svelte.js ├── 05-daterange.test.svelte.js └── vitest-setup.js └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | end_of_line = lf 4 | insert_final_newline = true 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.{svelte,js,css,md}] 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mskocik 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | /dist 11 | 12 | # OS 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Env 17 | .env 18 | .env.* 19 | !.env.example 20 | !.env.test 21 | 22 | # Vite 23 | vite.config.js.timestamp-* 24 | vite.config.ts.timestamp-* 25 | 26 | /__old 27 | /docs 28 | *.todo 29 | src/routes/test/* 30 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 6.1 2 | 3 | - breaking: parameters passed to `actionRow` are now object for easier usage 4 | - feat: add `children` slot 5 | - feat: add `.picker-active` class to main component wrapper for easier styling of `children` when popup visible 6 | 7 | ## 6.0 8 | 9 | - breaking: svelte 5 only 10 | - breaking: remove `initialDate` property in favor of function bindings (https://svelte.dev/docs/svelte/bind#Function-bindings) 11 | - breaking: fix wrapper class names typos: `std-component-wrap` to `sdt-component-wrap` and `std-calendar-wrap` to `sdt-calendar-wrap` 12 | - breaking: event emitting logic (on every change there is event emitted, if it's not desired, disable autocommit) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📅 Svelty Picker [![NPM version](https://img.shields.io/npm/v/svelty-picker.svg?style=flat)](https://www.npmjs.org/package/svelty-picker) 2 | 3 | Simple date & time picker implemented in svelte. 4 | 5 | Features: 6 | - date/time/datetime/range picker mode 7 | - various formatting options 8 | - keyboard navigation 9 | - replacable slots 10 | - themable 11 | - customizable disabled dates 12 | 13 | ## ⚙️ Install 14 | 15 | ```js 16 | npm install svelty-picker 17 | ``` 18 | 19 | If you need svelte 4 version: 20 | 21 | ```js 22 | npm install svelty-picker@5 23 | ``` 24 | 25 | ### Documentation 26 | 27 | For more details check the [documentation](https://svelty-picker.vercel.app/) 28 | 29 | ## 🏆 Thanks to: 30 | 31 | - [Bootstrap datepicker](https://github.com/smalot/bootstrap-datetimepicker/blob/master/js/bootstrap-datetimepicker.js) for some internal date and format handling 32 | 33 | ## Licence 34 | 35 | MIT 36 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "resolveJsonModule": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "checkJs": true, 11 | "moduleResolution": "bundler", 12 | "strictNullChecks": true, 13 | "exactOptionalPropertyTypes": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mdsvex.config.js: -------------------------------------------------------------------------------- 1 | import { defineMDSveXConfig } from 'mdsvex'; 2 | import highlighter from './src/utils/codeHighlighter.js'; 3 | import autolinkHeadings from 'rehype-autolink-headings'; 4 | import slugPlugin from 'rehype-slug'; 5 | 6 | // const __dirname = resolve(); 7 | 8 | const config = defineMDSveXConfig({ 9 | extensions: ['.svelte.md'], 10 | highlight: { 11 | highlighter, 12 | }, 13 | rehypePlugins: [ 14 | slugPlugin, 15 | [ 16 | autolinkHeadings, 17 | { 18 | behavior: 'wrap', 19 | }, 20 | ], 21 | ] 22 | // layout: join(__dirname, './src/lib/components/MarkdownLayout.svelte'), 23 | }); 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelty-picker", 3 | "description": "Sweet date/time picker written in svelte", 4 | "version": "6.1.5", 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build && npm run package", 8 | "preview": "vite preview", 9 | "package": "svelte-kit sync && svelte-package && publint", 10 | "prepublishOnly": "npm run package", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", 13 | "test": "vitest", 14 | "test:ui": "vitest --ui", 15 | "test:run": "vitest run" 16 | }, 17 | "files": [ 18 | "dist", 19 | "!dist/**/*.test.*", 20 | "!dist/**/*.spec.*" 21 | ], 22 | "sideEffects": [ 23 | "**/*.css" 24 | ], 25 | "svelte": "./dist/index.js", 26 | "types": "./dist/index.d.ts", 27 | "type": "module", 28 | "exports": { 29 | ".": { 30 | "types": "./dist/index.d.ts", 31 | "svelte": "./dist/index.js", 32 | "default": "./dist/index.js" 33 | }, 34 | "./i18n": { 35 | "types": "./dist/i18n/index.d.ts", 36 | "default": "./dist/i18n/index.js" 37 | }, 38 | "./element": { 39 | "types": "./dist/utils/custom-element.d.ts", 40 | "default": "./dist/utils/custom-element.js" 41 | } 42 | }, 43 | "peerDependencies": { 44 | "svelte": "^5.0.0" 45 | }, 46 | "devDependencies": { 47 | "@shikijs/transformers": "^1.29.2", 48 | "@sveltejs/adapter-auto": "^3.3.1", 49 | "@sveltejs/adapter-vercel": "^5.6.3", 50 | "@sveltejs/kit": "^2.17.2", 51 | "@sveltejs/package": "^2.3.10", 52 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 53 | "@testing-library/jest-dom": "^6.6.3", 54 | "@testing-library/svelte": "^5.2.7", 55 | "@testing-library/user-event": "^14.6.1", 56 | "@vitest/browser": "^2.1.9", 57 | "jsdom": "^25.0.1", 58 | "mdsvex": "^0.11.2", 59 | "playwright": "^1.50.1", 60 | "publint": "^0.2.12", 61 | "rehype-autolink-headings": "^7.1.0", 62 | "rehype-slug": "^6.0.0", 63 | "shiki": "^1.29.2", 64 | "svelte": "^5.20.4", 65 | "svelte-check": "^4.1.4", 66 | "typescript": "^5.7.3", 67 | "vite": "^6.2.0", 68 | "vitest": "^2.1.9", 69 | "vitest-browser-svelte": "^0.0.1" 70 | }, 71 | "dependencies": { 72 | "@floating-ui/dom": "^1.6.13" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 30 | 38 | %sveltekit.head% 39 | 40 | 41 |
%sveltekit.body%
42 | 43 | 44 | -------------------------------------------------------------------------------- /src/lib/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | .vscode 10 | -------------------------------------------------------------------------------- /src/lib/components/Time.svelte: -------------------------------------------------------------------------------- 1 | 384 | 385 |
386 |
387 | {#if hasDateComponent} 388 | 397 | {/if} 398 | {#if !hourOnly} 399 | 407 | : 408 | 416 | {:else} 417 | {view(selectedHour, showMeridian)} 418 | {#if showMeridian} 419 | {(isPM ? i18n.meridiem[1] : i18n.meridiem[0]).toUpperCase()} 420 | {:else} 421 | : 422 | 00 423 | {/if} 424 | {/if} 425 | {#if showMeridian} 426 |
427 | 434 |
435 | {/if} 436 |
437 | 438 | 439 | 440 |
{ e.preventDefault(); onClick(e) } } 445 | onmousedown={onToggleMove} 446 | onmousemove={e => { handleMoveMove && onClick(e) }} 447 | onmouseup={onToggleMove} 448 | > 449 |
450 |
451 |
452 |
453 | {#each pos as p, i(p.val)} 454 | 459 | {/each} 460 | {#each innerHours as p, i} 461 | 466 | {/each} 467 |
468 |
469 | 470 | 613 | -------------------------------------------------------------------------------- /src/lib/i18n/index.js: -------------------------------------------------------------------------------- 1 | //============================================================== 2 | /* * 3 | * always keep sorted alphabetically, when adding new locale * 4 | * */ 5 | //============================================================== 6 | 7 | /** 8 | * @typedef {object} i18nType 9 | * @property {string[]} days 10 | * @property {string[]} daysShort 11 | * @property {string[]} daysMin 12 | * @property {string[]} months 13 | * @property {string[]} monthsShort 14 | * @property {string[]} meridiem 15 | * @property {string[]} suffix 16 | * @property {string} todayBtn 17 | * @property {string} clearBtn 18 | * @property {string} okBtn 19 | * @property {string} cancelBtn 20 | * @property {string} timeView 21 | * @property {string} backToDate 22 | */ 23 | 24 | /** 25 | * @type {i18nType} Arabic translation by Amine Laaraf 26 | */ 27 | export const ar_DZ = { 28 | days: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'], 29 | daysShort: ['أحد', 'اثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت', 'أحد'], 30 | daysMin: ['أح', 'اث', 'ثل', 'أر', 'خم', 'جم', 'سب', 'أح'], 31 | months: ['جانفي', 'فيفري', 'مارس', 'أفريل', 'ماي', 'جوان', 'جويلية', 'أوت', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'], 32 | monthsShort: ['جان', 'فيف', 'مار', 'أفر', 'ماي', 'جوا', 'جوي', 'أوت', 'سبت', 'أكت', 'نوف', 'ديس'], 33 | meridiem: ['صباح', 'مساء'], 34 | suffix: ['', '', '', ''], 35 | todayBtn: 'اليوم', 36 | clearBtn: 'مسح', 37 | okBtn: 'تأكيد', 38 | cancelBtn: 'إلغاء', 39 | timeView: 'عرض الوقت', 40 | backToDate: 'العودة إلى عرض التقويم' 41 | }; 42 | 43 | /** 44 | * @type {i18nType} Arabic translation by Amine Laaraf 45 | */ 46 | export const ar_MR = { 47 | days: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'], 48 | daysShort: ['أحد', 'اثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت', 'أحد'], 49 | daysMin: ['أح', 'اث', 'ثل', 'أر', 'خم', 'جم', 'سب', 'أح'], 50 | months: ['يناير', 'فبراير', 'مارس', 'أبريل', 'ماي', 'يونيو', 'يوليوز', 'غشت', 'شتنبر', 'أكتوبر', 'نونبر', 'دجنبر'], 51 | monthsShort: ['ينا', 'فبر', 'مار', 'أبر', 'ماي', 'يون', 'يول', 'غش', 'شت', 'أكت', 'نون', 'دج'], 52 | meridiem: ['صباح', 'مساء'], 53 | suffix: ['', '', '', ''], 54 | todayBtn: 'اليوم', 55 | clearBtn: 'مسح', 56 | okBtn: 'تأكيد', 57 | cancelBtn: 'إلغاء', 58 | timeView: 'عرض الوقت', 59 | backToDate: 'العودة إلى عرض التقويم' 60 | }; 61 | 62 | /** 63 | * @type {i18nType} Arabic translation by Amine Laaraf 64 | */ 65 | export const ar_YE = { 66 | days: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'], 67 | daysShort: ['أحد', 'اثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت', 'أحد'], 68 | daysMin: ['أح', 'اث', 'ثل', 'أر', 'خم', 'جم', 'سب', 'أح'], 69 | months: ['يناير', 'فبراير', 'مارس', 'إبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'], 70 | monthsShort: ['ينا', 'فبر', 'مار', 'إبر', 'ماي', 'يون', 'يول', 'أغس', 'سبت', 'أكت', 'نوف', 'ديس'], 71 | meridiem: ['صباح', 'مساء'], 72 | suffix: ['', '', '', ''], 73 | todayBtn: 'اليوم', 74 | clearBtn: 'مسح', 75 | okBtn: 'تأكيد', 76 | cancelBtn: 'إلغاء', 77 | timeView: 'عرض الوقت', 78 | backToDate: 'العودة إلى عرض التقويم' 79 | } 80 | 81 | /** 82 | * @type {i18nType} Czech translation 83 | */ 84 | export const cz = { 85 | days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], 86 | daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], 87 | daysMin: ['Ne', 'Po', 'Út', 'St', 'Čt', 'Pá', 'So', 'Ne'], 88 | months: ['Leden', 'Únor', 'Březen', 'Duben', 'Květen', 'Červen', 'Červenec', 'Srpen', 'Září', 'Říjen', 'Listopad', 'Prosinec'], 89 | monthsShort: ['Led', 'Úno', 'Bře', 'Dub', 'Kvě', 'Čer', 'Čer', 'Srp', 'Zář', 'Říj', 'List', 'Pro'], 90 | meridiem: ['am', 'pm'], 91 | suffix: ['st', 'nd', 'rd', 'th'], 92 | todayBtn: 'Dnes', 93 | clearBtn: 'Smazat', 94 | okBtn: 'Ok', 95 | cancelBtn: 'Zrušit', 96 | timeView: 'Zobrazit hodiny', 97 | backToDate: 'Zpátky na kalendář' 98 | } 99 | 100 | /** 101 | * @type {i18nType} German translation by emroc GmbH 102 | */ 103 | export const de = { 104 | days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'], 105 | daysShort: ['Son', 'Mon', 'Die', 'Mie', 'Don', 'Fre', 'Sam', 'Son'], 106 | daysMin: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'], 107 | months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], 108 | monthsShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], 109 | meridiem: ['am', 'pm'], 110 | suffix: ['', '', '', ''], 111 | todayBtn: 'Heute', 112 | clearBtn: 'Zurücksetzen', 113 | okBtn: 'Ok', 114 | cancelBtn: 'Abbrechen', 115 | timeView: 'Zeitansicht anzeigen', 116 | backToDate: 'Zurück zur Kalenderansicht' 117 | } 118 | 119 | /** 120 | * @type {i18nType} English translation 121 | */ 122 | export const en = { 123 | days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], 124 | daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], 125 | daysMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'], 126 | months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], 127 | monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 128 | meridiem: ['am', 'pm'], 129 | suffix: ['st', 'nd', 'rd', 'th'], 130 | todayBtn: 'Today', 131 | clearBtn: 'Clear', 132 | okBtn: 'Ok', 133 | cancelBtn: 'Cancel', 134 | timeView: 'Show time view', 135 | backToDate: 'Back to calendar view' 136 | } 137 | 138 | 139 | /** 140 | * @type {i18nType} Spanish translation by markoan 141 | */ 142 | 143 | export const es = { 144 | days: ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'], 145 | daysShort: ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'], 146 | daysMin: ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa', 'Do'], 147 | months: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'], 148 | monthsShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'], 149 | meridiem: ['am', 'pm'], 150 | suffix: ['o', '', '', ''], 151 | todayBtn: 'Hoy', 152 | clearBtn: 'Borrar', 153 | okBtn: 'Aceptar', 154 | cancelBtn: 'Cancelar', 155 | timeView: 'Mostrar hora', 156 | backToDate: 'Regresar al calendario' 157 | } 158 | 159 | /** 160 | * @type {i18nType} Persian (Farsi) translation by sadegh19b 161 | */ 162 | export const fa = { 163 | days: ['یکشنبه', 'دوشنبه', 'سه‌شنبه‌', 'چهارشنبه', 'پنج‌شنبه', 'جمعه', 'شنبه', 'یکشنبه'], 164 | daysShort: ['یکش', 'دوش', 'سه‌ش', 'چهار', 'پنج', 'جمع', 'شنب', 'یکش'], 165 | daysMin: ['یک', 'دو', 'سه', 'چا', 'پن', 'جم', 'شن', 'یک'], 166 | months: ['ژانویه', 'فوریه', 'مارس', 'آپریل', 'می', 'ژوئن', 'جولای', 'آگوست', 'سپتامبر', 'اکتبر', 'نوامبر', 'دسامبر'], 167 | monthsShort: ['ژان', 'فور', 'مار', 'آپر', 'می', 'ژو', 'جول', 'آگو', 'سپت', 'اکت', 'نوا', 'دسا'], 168 | meridiem: ['ق.ض', 'ب.ض'], 169 | suffix: ['st', 'nd', 'rd', 'th'], 170 | todayBtn: 'امروز', 171 | clearBtn: 'پاک‌کردن', 172 | okBtn: 'تایید', 173 | cancelBtn: 'لغو', 174 | timeView: 'نمایش بخش زمان', 175 | backToDate: 'بازگشت به بخش تقویم' 176 | } 177 | 178 | /** 179 | * @type {i18nType} French translation by Tuditi 180 | */ 181 | export const fr = { 182 | days: ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'], 183 | daysShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'], 184 | daysMin: ['Di', 'Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa', 'Di'], 185 | months: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'], 186 | monthsShort: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'], 187 | meridiem: ['AM', 'PM'], 188 | suffix: ['er', 'ème', 'ème', 'ème'], 189 | todayBtn: 'Aujourd\'hui', 190 | clearBtn: 'Effacer', 191 | okBtn: 'OK', 192 | cancelBtn: 'Annuler', 193 | timeView: 'Afficher l\'heure', 194 | backToDate: 'Retour au calendrier' 195 | } 196 | 197 | /** 198 | * @type {i18nType} Croatian translation by AntonioStipic 199 | */ 200 | export const hr = { 201 | days: ['Nedjelja', 'Ponedjeljak', 'Utorak', 'Srijeda', 'Četvrtak', 'Petak', 'Subota', 'Nedjelja'], 202 | daysShort: ['Ned', 'Pon', 'Uto', 'Sri', 'Čet', 'Pet', 'Sub', 'Ned'], 203 | daysMin: ['Ne', 'Po', 'Ut', 'Sr', 'Čt', 'Pt', 'Su', 'Ne'], 204 | months: ['Siječanj', 'Veljača', 'Ožujak', 'Travanj', 'Svibanj', 'Lipanj', 'Srpanj', 'Kolovoz', 'Rujan', 'Listopad', 'Studeni', 'Prosinac'], 205 | monthsShort: ['Sij', 'Vel', 'Ožu', 'Tra', 'Svi', 'Lip', 'Srp', 'Kol', 'Ruj', 'Lis', 'Stu', 'Pro'], 206 | meridiem: ['am', 'pm'], 207 | suffix: ['', '', '', ''], 208 | todayBtn: 'Danas', 209 | clearBtn: 'Očisti', 210 | okBtn: 'OK', 211 | cancelBtn: 'Prekid', 212 | timeView: 'Prikaži vrijeme', 213 | backToDate: 'Nazad na kalendar' 214 | } 215 | 216 | /** 217 | * @type {i18nType} Hungarian translation by Tuditi 218 | */ 219 | export const hu = { 220 | days: ['Vasárnap', 'Hétfő', 'Kedd', 'Szerda', 'Csütörtök', 'Péntek', 'Szombat', 'Vasárnap'], 221 | daysShort: ['V', 'H', 'K', 'Sze', 'Cs', 'P', 'Szo', 'V'], 222 | daysMin: ['V', 'H', 'K', 'Sze', 'Cs', 'P', 'Szo', 'V'], 223 | months: ['Január', 'Február', 'Március', 'Április', 'Május', 'Június', 'Július', 'Augusztus', 'Szeptember', 'Október', 'November', 'December'], 224 | monthsShort: ['Jan', 'Feb', 'Már', 'Ápr', 'Máj', 'Jún', 'Júl', 'Aug', 'Szept', 'Okt', 'Nov', 'Dec'], 225 | meridiem: ['de', 'du'], 226 | suffix: ['.', '.', '.', '.'], 227 | todayBtn: 'Ma', 228 | clearBtn: 'Törlés', 229 | okBtn: 'OK', 230 | cancelBtn: 'Áthúz', 231 | timeView: 'Óra nézet', 232 | backToDate: 'Vissza a naptárhoz' 233 | }; 234 | 235 | /** 236 | * @type {i18nType} Indonesian translation 237 | */ 238 | export const id = { 239 | days: ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'], 240 | daysShort: ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'], 241 | daysMin: ['Mn', 'Sn', 'Sl', 'Rb', 'Km', 'Jm', 'Sb', 'Mn'], 242 | months: ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'], 243 | monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], 244 | meridiem: ['am', 'pm'], 245 | suffix: ['st', 'nd', 'rd', 'th'], 246 | todayBtn: 'Hari Ini', 247 | clearBtn: 'Hapus', 248 | okBtn: 'Mengkonfirmasi', 249 | cancelBtn: 'Batal', 250 | timeView: 'Tampilkan tampilan waktu', 251 | backToDate: 'Kembali ke tampilan kalender' 252 | } 253 | 254 | /** 255 | * @type {i18nType} Japanese translation by aska 256 | */ 257 | export const jp = { 258 | days: ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'], 259 | daysShort: ['日曜', '月曜', '火曜', '水曜', '木曜', '金曜', '土曜', '日曜'], 260 | daysMin: ['日', '月', '火', '水', '木', '金', '土', '日'], 261 | months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'], 262 | monthsShort: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'], 263 | meridiem: ['am', 'pm'], 264 | suffix: ['', '', '', ''], 265 | todayBtn: '今日', 266 | clearBtn: 'クリア', 267 | okBtn: '確認する', 268 | cancelBtn: 'キャンセル', 269 | timeView: 'タイムを表示', 270 | backToDate: 'カレンダーに戻る' 271 | } 272 | 273 | /** 274 | * @type {i18nType} Dutch Translation by Tuditi 275 | */ 276 | export const nl = { 277 | days: ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag', 'Zondag'], 278 | daysShort: ['Zon', 'Maa', 'Din', 'Woe', 'Don', 'Vri', 'Zat', 'Zon'], 279 | daysMin: ['Zo', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Za', 'Zo'], 280 | months: ['Januari', 'Februari', 'Maart', 'April', 'Mei', 'Juni', 'Juli', 'Augustus', 'September', 'Oktober', 'November', 'December'], 281 | monthsShort: ['Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'], 282 | meridiem: ['AM', 'PM'], 283 | suffix: ['e', 'e', 'e', 'e'], 284 | todayBtn: 'Vandaag', 285 | clearBtn: 'Wissen', 286 | okBtn: 'OK', 287 | cancelBtn: 'Annuleren', 288 | timeView: 'Uurweergave', 289 | backToDate: 'Terug naar de kalender' 290 | }; 291 | 292 | /** 293 | * @type {i18nType} Slovak Translation 294 | */ 295 | export const sk = { 296 | days: ['Nedeľa', 'Pondelok', 'Utorok', 'Streda', 'Štvrtok', 'Piatok', 'Sobota', 'Nedeľa'], 297 | daysShort: ['Ned', 'Pon', 'Uto', 'Str', 'Štv', 'Pia', 'Sob', 'Ned'], 298 | daysMin: ['Ne', 'Po', 'Ut', 'St', 'Št', 'Pi', 'So', 'Ne'], 299 | months: ['Január', 'Február', 'Marec', 'Apríl', 'Máj', 'Jún', 'Júl', 'August', 'September', 'Október', 'November', 'December'], 300 | monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Máj', 'Jún', 'Júl', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'], 301 | meridiem: ['am', 'pm'], 302 | suffix: ['st', 'nd', 'rd', 'th'], 303 | todayBtn: 'Dnes', 304 | clearBtn: 'Zmazať', 305 | okBtn: 'Ok', 306 | cancelBtn: 'Zrušiť', 307 | timeView: 'Zobraziť hodiny', 308 | backToDate: 'Späť na kalendár' 309 | } 310 | 311 | /** 312 | * @type {i18nType} Korean translation 313 | */ 314 | export const ko = { 315 | days: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'], 316 | daysShort: ['일', '월', '화', '수', '목', '금', '토', '일'], 317 | daysMin: ['일', '월', '화', '수', '목', '금', '토', '일'], 318 | months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], 319 | monthsShort: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], 320 | meridiem: ['오전', '오후'], 321 | suffix: ['', '', '', ''], 322 | todayBtn: '오늘', 323 | clearBtn: '지우기', 324 | okBtn: '확인하다', 325 | cancelBtn: '취소', 326 | timeView: '시계보기', 327 | backToDate: '달력보기' 328 | } 329 | 330 | /** 331 | * @type {i18nType} Latin translation by PSYCHONOISE 332 | */ 333 | export const la = { 334 | days: ['Solis', 'Lunae', 'Martis', 'Mercuri', 'Jovis', 'Veneris', 'Saturni', 'Solis'], 335 | daysShort: ['Sol', 'Lun', 'Mar', 'Mer', 'Jov', 'Ven', 'Sat', 'Sol'], 336 | daysMin: ['So', 'Lu', 'Ma', 'Me', 'Jo', 'Ve', 'Sa', 'So'], 337 | months: ['Januarius', 'Februarius', 'Martius', 'Aprilis', 'Maius', 'Iunius', 'Iulius', 'Augustus', 'September', 'October', 'November', 'December'], 338 | monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Qui', 'Sex', 'Sep', 'Oct', 'Nov', 'Dec'], 339 | meridiem: ['am', 'pm'], 340 | suffix: ['us', 'us', 'us', 'us'], 341 | todayBtn: 'Hodie', 342 | clearBtn: 'Patet', 343 | okBtn: 'ОК', 344 | cancelBtn: 'Cancel', 345 | timeView: 'Ostende tempus visum', 346 | backToDate: 'Ad visum calendarium' 347 | } 348 | 349 | /** 350 | * @type {i18nType} Brazilian portuguese translation by IgorDalepiane 351 | */ 352 | export const pt_BR = { 353 | days: ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado', 'Domingo'], 354 | daysShort: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'], 355 | daysMin: ['Do', 'Se', 'Te', 'Qu', 'Qu', 'Se', 'Sá', 'Do'], 356 | months: ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'], 357 | monthsShort: ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'], 358 | meridiem: ['am', 'pm'], 359 | suffix: ['º', 'º', 'º', 'º'], 360 | todayBtn: 'Hoje', 361 | clearBtn: 'Limpar', 362 | okBtn: 'OK', 363 | cancelBtn: 'Cancelar', 364 | timeView: 'Mostrar hora', 365 | backToDate: 'Voltar para o calendário' 366 | } 367 | 368 | /** 369 | * @type {i18nType} Russian translation by PSYCHONOISE 370 | */ 371 | export const ru = { 372 | days: ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'], 373 | daysShort: ['Вск', 'Пнд', 'Втр', 'Срд', 'Чтв', 'Птн', 'Сбт', 'Вск'], 374 | daysMin: ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'], 375 | months: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'], 376 | monthsShort: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'], 377 | meridiem: ['AM', 'PM'], 378 | suffix: ['.', '.', '.', '.'], 379 | todayBtn: 'Сегодня', 380 | clearBtn: 'Очистить', 381 | okBtn: 'ОК', 382 | cancelBtn: 'Отмена', 383 | timeView: 'Показать вид времени', 384 | backToDate: 'Вернуться к виду календаря' 385 | } 386 | 387 | /** 388 | * @type {i18nType} Italian translation by MarkNerdi 389 | */ 390 | export const it = { 391 | days: ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'], 392 | daysShort: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'], 393 | daysMin: ['Do', 'Lu', 'Ma', 'Me', 'Gi', 'Ve', 'Sa', 'Do'], 394 | months: ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'], 395 | monthsShort: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'], 396 | meridiem: ['am', 'pm'], 397 | suffix: ['º', 'º', 'º', 'º'], 398 | todayBtn: 'Oggi', 399 | clearBtn: 'Cancella', 400 | okBtn: 'Ok', 401 | cancelBtn: 'Annulla', 402 | timeView: 'Mostra orario', 403 | backToDate: 'Torna alla vista calendario' 404 | } 405 | 406 | /** 407 | * @type {i18nType} Turkish translation by semih-ky 408 | */ 409 | export const tr = { 410 | days: ['Pazar', 'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'], 411 | daysShort: ['Paz', 'Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cmt', 'Paz'], 412 | daysMin: ['Pa', 'Pz', 'Sa', 'Ça', 'Pe', 'Cu', 'Cm', 'Pa'], 413 | months: ['Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'], 414 | monthsShort: ['Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'Ağu', 'Eyl', 'Eki', 'Kas', 'Ara'], 415 | meridiem: ['öö', 'ös'], 416 | suffix: ['.', '.', '.', '.'], 417 | todayBtn: 'Bugün', 418 | clearBtn: 'Temizle', 419 | okBtn: 'Ok', 420 | cancelBtn: 'İptal', 421 | timeView: 'Zaman görünümünü göster', 422 | backToDate: 'Takvim görünümüne geri dön' 423 | } 424 | 425 | /** 426 | * @type {i18nType} Swedish translation by brantsrasmus 427 | */ 428 | export const sv = { 429 | days: ['Söndag', 'Måndag', 'Tisdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lördag', 'Söndag'], 430 | daysShort: ['Sön', 'Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör', 'Sön'], 431 | daysMin: ['Sö', 'Må', 'Ti', 'On', 'To', 'Fr', 'Lö', 'Sö'], 432 | months: ['Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni', 'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December'], 433 | monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'], 434 | meridiem: ['am', 'pm'], 435 | suffix: ['.', '.', '.', '.'], 436 | todayBtn: 'Idag', 437 | clearBtn: 'Rensa', 438 | okBtn: 'Ok', 439 | cancelBtn: 'Avbryt', 440 | timeView: 'Visa tid', 441 | backToDate: 'Tillbaka till kalender' 442 | } 443 | 444 | /** 445 | * @type {i18nType} Danish translation by brantsrasmus 446 | */ 447 | export const da = { 448 | days: ['Søndag', 'Mandag', 'Tirsdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lørdag', 'Søndag'], 449 | daysShort: ['Søn', 'Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør', 'Søn'], 450 | daysMin: ['Sø', 'Ma', 'Ti', 'On', 'To', 'Fr', 'Lø', 'Sø'], 451 | months: ['Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'], 452 | monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'], 453 | meridiem: ['am', 'pm'], 454 | suffix: ['.', '.', '.', '.'], 455 | todayBtn: 'I dag', 456 | clearBtn: 'Slet', 457 | okBtn: 'Ok', 458 | cancelBtn: 'Annuller', 459 | timeView: 'Vis tid', 460 | backToDate: 'Tilbage til kalenderen' 461 | } 462 | 463 | /** 464 | * @type {i18nType} Thai translation by kodaicoder 465 | */ 466 | export const th = { 467 | days: ['อาทิตย์', 'จันทร์', 'อังคาร', 'พุธ', 'พฤหัสบดี', 'ศุกร์', 'เสาร์', 'อาทิตย์'], 468 | daysShort: ['อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.', 'อา.'], 469 | daysMin: ['อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.', 'อา.'], 470 | months: ['มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'], 471 | monthsShort: ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'], 472 | meridiem: ['am', 'pm'], 473 | suffix: ['', '', '', ''], 474 | todayBtn: 'วันนี้', 475 | clearBtn: 'ล้างข้อมูล', 476 | okBtn: 'ยืนยัน', 477 | cancelBtn: 'ปิด', 478 | timeView: 'แสดงหน้าเลือกเวลา', 479 | backToDate: 'กลับไปหน้าปฏิทิน' 480 | } 481 | 482 | /** 483 | * @type {i18nType} Chinese Simplified translation by shiroko 484 | */ 485 | export const zh_CN = { 486 | days: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'], 487 | daysShort: ['周日', '周一', '周二', '周三', '周四', '周五', '周六', '周日'], 488 | daysMin: ['日', '一', '二', '三', '四', '五', '六', '日'], 489 | months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'], 490 | monthsShort: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'], 491 | meridiem: ['上午', '下午'], 492 | suffix: ['', '', '', ''], 493 | todayBtn: '今天', 494 | clearBtn: '清空', 495 | okBtn: '好的', 496 | cancelBtn: '取消', 497 | timeView: '显示时间选择', 498 | backToDate: '回退到日历选项卡' 499 | } 500 | 501 | /** 502 | * IMPORTANT: always keep it sorted alphabetically 503 | * 504 | * @type {Record} 505 | */ 506 | export default { 507 | ar_DZ, ar_MR, ar_YE, cz, da, de, en, es, fr, hr, hu, id, it, jp, ko, la, nl, pt_BR, ru, sk, sv, th, tr, zh_CN 508 | } -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import Component, { config } from './components/SveltyPicker.svelte'; 3 | 4 | export default Component; 5 | export { config }; 6 | export { parseDate, formatDate } from './utils/dateUtils.js'; 7 | -------------------------------------------------------------------------------- /src/lib/settings.js: -------------------------------------------------------------------------------- 1 | import { en } from '$lib/i18n/index.js'; 2 | 3 | export default { 4 | theme: 'sdt-calendar-colors', 5 | format: 'yyyy-mm-dd', 6 | formatType: 'standard', 7 | displayFormat: null, 8 | displayFormatType: null, 9 | minuteIncrement: 1, 10 | weekStart: 1, 11 | inputClasses: '', 12 | todayBtnClasses: 'sdt-action-btn sdt-today-btn', 13 | clearBtnClasses: 'sdt-action-btn sdt-clear-btn', 14 | hourOnly: false, 15 | todayBtn: true, 16 | clearBtn: true, 17 | clearToggle: true, 18 | autocommit: true, 19 | i18n: en 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/types/internal.d.ts: -------------------------------------------------------------------------------- 1 | export type UpdateProp = { 2 | type: 'date' | 'datetime' | 'hour' | 'minute', 3 | date: Date, 4 | isKeyboard: boolean, 5 | dateIndex?: number 6 | } 7 | 8 | export type DateChange = { 9 | value: string|string[]|null, 10 | dateValue: Date|Date[]|null, 11 | displayValue: string, 12 | valueFormat: string, 13 | displayFormat: string | null, 14 | event: 'date'|'hour'|'minute'|'datetime' 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/utils/actions.js: -------------------------------------------------------------------------------- 1 | import { computePosition, autoUpdate, shift, flip } from '@floating-ui/dom'; 2 | 3 | /** 4 | * @param {HTMLDivElement} node 5 | * @returns void 6 | */ 7 | export function usePosition(node) { 8 | const inputElement = node.parentElement?.querySelector('input[type=text]'); 9 | if (!inputElement) return; 10 | 11 | const removeFloating = autoUpdate(inputElement, node, () => 12 | computePosition(inputElement, node, { 13 | placement: 'bottom-start', 14 | middleware: [ 15 | shift({ 16 | padding: 5 17 | }), 18 | flip() 19 | ] 20 | }).then(({x, y}) => { 21 | Object.assign(node.style, { 22 | left: `${x}px`, 23 | top: `${y}px`, 24 | }); 25 | }) 26 | ) 27 | 28 | return { 29 | destroy() { 30 | removeFloating(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const MODE_DECADE = 0; 2 | export const MODE_YEAR = 1; 3 | export const MODE_MONTH = 2; 4 | 5 | export const STARTVIEW_TIME = 3; 6 | -------------------------------------------------------------------------------- /src/lib/utils/custom-element.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | const OPTION_LIST = [ 4 | 'value', 'name', 'placeholder', 'start-date', 'end-date', 'disabled', 'input-classes', 5 | 'mode', 'format', 'format-type', 'display-format', 'display-format-type', 'week-start', 'today-btn', 'clear-btn', 'autoclose', 'required' 6 | ]; 7 | 8 | function formatValue(name, value) { 9 | switch (name) { 10 | case 'value': 11 | return value || ''; 12 | case 'required': 13 | case 'disabled': 14 | case 'today-btn': 15 | case 'clear-btn': 16 | case 'autoclose': 17 | return value !== null && value !== 'false'; 18 | case 'weekStart': 19 | return parseInt(value); 20 | } 21 | return value; 22 | } 23 | 24 | function formatProp(name) { 25 | if (name.includes('-')) return name.split('-').reduce((res, w, i) => { 26 | if (i) w = w[0].toUpperCase() + w.substr(1); 27 | return res+w; 28 | }, ''); 29 | return name; 30 | } 31 | 32 | let _component; 33 | let _config; 34 | 35 | class PickerElement extends HTMLElement { 36 | constructor() { 37 | super(); 38 | this.picker = null; 39 | this.valueElement = null; 40 | this.displayElement = null; 41 | 42 | const simpleProps = [ 43 | 'value', 44 | 'name', 'placeholder', 'mode', 'format' 45 | ].reduce((res, name) => { 46 | res[name] = { 47 | get() { 48 | return formatValue(name, this.getAttribute(name)) 49 | }, 50 | set(val) { 51 | this.setAttribute(name, val) 52 | } 53 | } 54 | return res; 55 | }, {}); 56 | const baseProps = { 57 | 'form': { 58 | get() { 59 | return this.closest('form'); 60 | } 61 | }, 62 | 'weekStart': { 63 | get() { 64 | return this.getAttribute('week-start'); 65 | }, 66 | set(val) { 67 | this.setAttribute('week-start', val); 68 | } 69 | }, 70 | 'startDate': { 71 | get() { 72 | return this.getAttribute('start-date'); 73 | }, 74 | set(val) { 75 | if (val) this.setAttribute('start-date', val); 76 | !val && this.removeAttribute('start-date'); 77 | } 78 | }, 79 | 'endDate': { 80 | get() { 81 | return this.getAttribute('end-date'); 82 | }, 83 | set(val) { 84 | val && this.setAttribute('end-date', val); 85 | !val && this.removeAttribute('end-date'); 86 | } 87 | }, 88 | 'inputClasses': { 89 | get() { 90 | return this.getAttribute('input-classes') 91 | }, 92 | set(val) { 93 | val && this.setAttribute('input-classes', val) 94 | !val && this.removeAttribute('input-classes'); 95 | } 96 | }, 97 | 'formatType': { 98 | get() { 99 | return this.getAttribute('format-type'); 100 | }, 101 | set(val) { 102 | val && ['standard', 'php'].includes(val) && this.setAttribute('format-type', val); 103 | !val && this.removeAttribute('format-type'); 104 | } 105 | }, 106 | 'displayFormat': { 107 | get() { 108 | return this.getAttribute('display-format'); 109 | }, 110 | set(val) { 111 | val && this.setAttribute('display-format', val); 112 | !val && this.removeAttribute('display-format'); 113 | } 114 | }, 115 | 'displayFormatType': { 116 | get() { 117 | return this.getAttribute('display-format-type'); 118 | }, 119 | set(val) { 120 | val && ['standard', 'php'].includes(val) && this.setAttribute('display-format-type', val); 121 | !val && this.removeAttribute('display-format-type'); 122 | } 123 | } 124 | } 125 | const boolProps = ['required', 'disabled', 'today-btn', 'clear-btn','autoclose'] 126 | .reduce((res, propName) => { 127 | const formatted = formatProp(propName); 128 | res[formatted] = { 129 | get() { 130 | const hasProp = this.hasAttribute(propName); 131 | const notFalse = hasProp ? this.getAttribute(propName) !== 'false' : true; 132 | return !hasProp ? _config[formatted] : notFalse; 133 | }, 134 | set(value) { 135 | if (!value) { 136 | if (this.hasAttribute(propName)) { 137 | this.removeAttribute(propName); 138 | } else { 139 | // set directly to false, when config default is true 140 | this.picker.$set({ [formatted]: value }); 141 | } 142 | } else { 143 | this.setAttribute(propName, value = true ? '' : value); 144 | } 145 | } 146 | } 147 | return res; 148 | }, {}); 149 | Object.defineProperties(this, Object.assign({}, simpleProps, baseProps, boolProps)); 150 | } 151 | 152 | focus() { 153 | if (this.disabled) return; 154 | const input = this.querySelector('input[type="text"]'); 155 | input && input.focus(); 156 | } 157 | 158 | static get observedAttributes() { 159 | return OPTION_LIST; 160 | } 161 | 162 | attributeChangedCallback(name, oldValue, newValue) { 163 | if (this.picker && oldValue !== newValue) { 164 | this.picker.$set({ [formatProp(name)]: formatValue(name, newValue) }); 165 | } 166 | } 167 | 168 | connectedCallback() { 169 | setTimeout(() => this.init()); 170 | } 171 | 172 | init() { 173 | if (this.picker) return; 174 | const props = {}; 175 | for (const attr of OPTION_LIST) { 176 | if (this.hasAttribute(attr)) { 177 | props[formatProp(attr)] = formatValue(attr, this.getAttribute(attr)); 178 | } 179 | } 180 | // resolve existing elements 181 | this.valueElement = this.querySelector('input[type="hidden"]'); 182 | this.displayElement = this.querySelector('input[type="text"]'); 183 | if (this.valueElement) props.ce_valueElement = this.valueElement; 184 | if (this.displayElement) props.ce_displayElement = this.displayElement; 185 | 186 | this.picker = new _component({ 187 | target: this, 188 | props: props 189 | }); 190 | this.picker.$on('input', e => { 191 | this.setAttribute('value', e.detail || ''); 192 | this.dispatchEvent(new Event('input')); 193 | }); 194 | this.picker.$on('blur', e => { 195 | this.dispatchEvent(new Event('blur')); 196 | }); 197 | // bind from/to 198 | if (this.hasAttribute('from')) { 199 | const el = document.getElementById(this.getAttribute('from')); 200 | el.oninput = e => { 201 | this.picker.$set({ startDate: el.value }); 202 | } 203 | } 204 | if (this.hasAttribute('to')) { 205 | const el = document.getElementById(this.getAttribute('to')); 206 | el.oninput = e => { 207 | this.picker.$set({ endDate: el.value }); 208 | } 209 | } 210 | } 211 | 212 | disconnectedCallback() { 213 | this.picker && this.picker.$destroy(); 214 | } 215 | } 216 | 217 | /** 218 | * Define custom element 219 | * 220 | * @param {string} name name of custom element 221 | * @param {object} component `SveltyPicker` component 222 | * @param {config} globalConfig globally available config 223 | */ 224 | export function registerSveltyPicker(name, component, globalConfig) { 225 | _component = component; 226 | _config = globalConfig; 227 | window.customElements.define(name, PickerElement); 228 | } 229 | -------------------------------------------------------------------------------- /src/lib/utils/dateUtils.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("$lib/i18n").i18nType} i18nType */ 2 | 3 | /** 4 | * 5 | * @param {Date|string} date 6 | * @param {string} format 7 | * @param {i18nType} i18n 8 | * @param {string} type 9 | * @returns {Date} 10 | */ 11 | export function parseDate(date, format, i18n, type) { 12 | if (date instanceof Date) { 13 | return date; 14 | // const dateUTC = new Date(date.valueOf() + date.getTimezoneOffset() * 60000); 15 | // dateUTC.setMilliseconds(0); 16 | // return dateUTC; 17 | } 18 | const commonFormats = type === 'php' 19 | ? { date: 'Y-m-d', datetime: 'Y-m-d H:i', datetime_s: 'Y-m-d H:i:s' } 20 | : { date: 'yyyy-mm-dd', datetime: 'yyyy-mm-dd hh:ii', datetime_s: 'yyyy-mm-dd hh:ii:ss' }; 21 | /** @var {{ separators: string[], parts: string[]}} */ 22 | let parsedFormat; 23 | let useParsedTime; 24 | if (/^\d{4}\-\d{1,2}\-\d{1,2}$/.test(date)) { 25 | parsedFormat = formatHelper.parseFormat(commonFormats.date, type); 26 | } else 27 | if (/^\d{4}\-\d{1,2}\-\d{1,2}[T ]\d{1,2}\:\d{1,2}$/.test(date)) { 28 | parsedFormat = formatHelper.parseFormat(commonFormats.datetime, type); 29 | } else 30 | if (/^\d{4}\-\d{1,2}\-\d{1,2}[T ]\d{1,2}\:\d{1,2}\:\d{1,2}[Z]{0,1}$/.test(date)) { 31 | parsedFormat = formatHelper.parseFormat(commonFormats.datetime_s, type); 32 | } else 33 | // specific case when parsing time without 'nonPunctuation' ref #102 34 | if (/^([01]*\d|2[0-3])([0-5]\d)(?:[ ]([ap][m]|[AP][M]))?$/.test(date)) { 35 | useParsedTime = date.match(/^([01]*\d|2[0-3])([0-5]\d)(?:[ ]([ap][m]|[AP][M]))?$/)?.slice(1).filter(e => e); 36 | parsedFormat = formatHelper.parseFormat(format, type); 37 | 38 | } else { 39 | parsedFormat = formatHelper.parseFormat(format, type); 40 | } 41 | const parts = useParsedTime 42 | ? useParsedTime 43 | : (date && date.toString().match(formatHelper.nonpunctuation) || []); 44 | date = new Date(); // reset date 45 | date.setHours(0,0,0,0); 46 | /** @type {Record} */ 47 | const parsed = {}; 48 | const { setters_order, setters_map } = formatHelper.setters(type); 49 | let val, part; 50 | if (parts.length !== parsedFormat.parts.length && parsedFormat.parts.includes('S')) { // specific suffix parsing from string like '14th' 51 | const splitSuffix = parts[parsedFormat.parts.indexOf('S') - 1].match(/(\d+)([a-zA-Z]+)/)?.slice(1,3); 52 | // @ts-ignore 53 | parts.splice(parsedFormat.parts.indexOf('S') - 1, 1, ...splitSuffix); 54 | } 55 | if (parts.length === parsedFormat.parts.length) { 56 | for (var i = 0, cnt = parsedFormat.parts.length; i < cnt; i++) { 57 | val = parseInt(parts[i], 10); 58 | part = parsedFormat.parts[i]; 59 | if (isNaN(val)) { 60 | if (type === 'standard') { 61 | switch (part) { 62 | case 'MM': 63 | val = i18n.months.indexOf(parts[i]) + 1; 64 | break; 65 | case 'M': 66 | val= i18n.monthsShort.indexOf(parts[i]) + 1; 67 | break; 68 | case 'p': 69 | case 'P': 70 | val = i18n.meridiem.indexOf(parts[i].toLowerCase()); 71 | break; 72 | } 73 | } else { 74 | // php 75 | switch (part) { 76 | case 'D': 77 | val = i18n.daysShort.indexOf(parts[i]) + 1; 78 | break; 79 | case 'l': 80 | val = i18n.days.indexOf(parts[i]) + 1; 81 | break; 82 | case 'F': 83 | val = i18n.months.indexOf(parts[i]) + 1; 84 | break; 85 | case 'M': 86 | val= i18n.monthsShort.indexOf(parts[i]) + 1; 87 | break; 88 | case 'a': 89 | case 'A': 90 | val = i18n.meridiem.indexOf(parts[i].toLowerCase()); 91 | break; 92 | } 93 | } 94 | } 95 | parsed[part] = val; 96 | } 97 | for (var i = 0, s; i < setters_order.length; i++) { 98 | s = setters_order[i]; 99 | if (s in parsed && !isNaN(parsed[s])) 100 | setters_map[`${s}`] && setters_map[`${s}`](date, parsed[s]) 101 | } 102 | } 103 | return date; 104 | } 105 | 106 | /** 107 | * @param {Date} date 108 | * @param {string} format 109 | * @param {i18nType} i18n 110 | * @param {string} type 111 | * @returns {string} date' string representation 112 | */ 113 | export function formatDate(date, format, i18n, type) { 114 | if (date === null) { 115 | return ''; 116 | } 117 | const dateVal = date.getDate(); 118 | /** @type {Record} */ 119 | let val; 120 | if (type === 'standard') { 121 | val = { 122 | t: date.getTime(), 123 | // year 124 | yy: date.getFullYear().toString().substring(2), 125 | yyyy: date.getFullYear(), 126 | // month 127 | m: date.getMonth() + 1, 128 | M: i18n.monthsShort[date.getMonth()], 129 | MM: i18n.months[date.getMonth()], 130 | // day 131 | d: dateVal, 132 | D: i18n.daysShort[date.getDay()], 133 | DD: i18n.days[date.getDay()], 134 | S: (dateVal % 10 && dateVal % 10 < 4 && (dateVal < 10 || dateVal > 14) ? i18n.suffix[dateVal % 10 - 1] : i18n.suffix[i18n.suffix.length -1 ]), 135 | p: (i18n.meridiem.length === 2 ? i18n.meridiem[date.getHours() < 12 ? 0 : 1] : ''), 136 | // hour 137 | h: date.getHours(), 138 | // minute 139 | ii: (date.getMinutes() < 10 ? '0' : '') + date.getMinutes(), 140 | // second 141 | ss: (date.getUTCSeconds() < 10 ? '0' : '') + date.getUTCSeconds() 142 | }; 143 | 144 | if (i18n.meridiem.length === 2) { 145 | val.H = (val.h % 12 === 0 ? 12 : val.h % 12); 146 | } 147 | else { 148 | val.H = val.h; 149 | } 150 | val.HH = (val.H < 10 ? '0' : '') + val.H; 151 | val.P = val.p.toUpperCase(); 152 | val.hh = (val.h < 10 ? '0' : '') + val.h; 153 | val.i = val.ii; 154 | val.s = val.ss; 155 | val.dd = (val.d < 10 ? '0' : '') + val.d; 156 | val.mm = (val.m < 10 ? '0' : '') + val.m; 157 | } else if (type === 'php') { 158 | // php format 159 | val = { 160 | // year 161 | y: date.getFullYear().toString().substring(2), 162 | Y: date.getFullYear(), 163 | // month 164 | F: i18n.months[date.getMonth()], 165 | M: i18n.monthsShort[date.getMonth()], 166 | n: date.getMonth() + 1, 167 | t: getDaysInMonth(date.getFullYear(), date.getMonth()), 168 | // day 169 | j: date.getDate(), 170 | l: i18n.days[date.getDay()], 171 | D: i18n.daysShort[date.getDay()], 172 | w: date.getDay(), // 0 -> 6 173 | N: (date.getDay() === 0 ? 7 : date.getDay()), // 1 -> 7 174 | S: (dateVal % 10 && dateVal % 10 < 4 && (dateVal < 10 || dateVal > 14) ? i18n.suffix[dateVal % 10 - 1] : i18n.suffix[i18n.suffix.length -1 ]), 175 | // hour 176 | a: (i18n.meridiem.length === 2 ? i18n.meridiem[date.getHours() < 12 ? 0 : 1] : ''), 177 | g: (date.getHours() % 12 === 0 ? 12 : date.getHours() % 12), 178 | G: date.getHours(), 179 | // minute 180 | i: date.getMinutes(), 181 | // second 182 | s: date.getSeconds(), 183 | U: Math.floor(date.getTime() / 1000) 184 | }; 185 | val.m = (val.n < 10 ? '0' : '') + val.n; 186 | val.d = (val.j < 10 ? '0' : '') + val.j; 187 | val.A = val.a.toString().toUpperCase(); 188 | val.h = (val.g < 10 ? '0' : '') + val.g; 189 | val.H = (val.G < 10 ? '0' : '') + val.G; 190 | val.i = (val.i < 10 ? '0' : '') + val.i; 191 | val.s = (val.s < 10 ? '0' : '') + val.s; 192 | } else { 193 | throw new Error('Invalid format type.'); 194 | } 195 | let dateArr = []; 196 | const pFormat = formatHelper.parseFormat(format, type); 197 | for (var i = 0, cnt = pFormat.parts?.length || 0; i < cnt; i++) { 198 | if (pFormat.separators.length) { 199 | dateArr.push(pFormat.separators.shift()); 200 | } 201 | dateArr.push(val[pFormat.parts[i]]); 202 | } 203 | if (pFormat.separators.length) { 204 | dateArr.push(pFormat.separators.shift()); 205 | } 206 | return dateArr.join(''); 207 | } 208 | 209 | 210 | /** 211 | * @param {number} year 212 | * @param {number} month 213 | * @returns {number} 214 | */ 215 | export function getDaysInMonth(year, month) { 216 | const isLeapYear = (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)); 217 | return [31, (isLeapYear ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] 218 | } 219 | 220 | /** 221 | * Date comparison a < b 222 | * 223 | * @param {Date|string} a 224 | * @param {Date} b 225 | * @returns 226 | */ 227 | export function isLower(a, b) { 228 | if (!(a instanceof Date)) return false; 229 | return a.getFullYear() < b.getFullYear() 230 | || (a.getMonth() < b.getMonth() || a.getDate() < b.getDate()); 231 | } 232 | 233 | /** 234 | * Date comparison a > b 235 | * 236 | * @param {Date|string} a 237 | * @param {Date} b 238 | * @returns 239 | */ 240 | export function isGreater(a, b) { 241 | if (!(a instanceof Date)) return false; 242 | return a.getFullYear() > b.getFullYear() 243 | || (a.getMonth() > b.getMonth() || a.getDate() > b.getDate()); 244 | } 245 | 246 | /** 247 | * @callback MapperFunction 248 | * @param {Date} d date 249 | * @param {number} v value to be set according to format 250 | * @returns void 251 | * 252 | * @typedef {Record} SetterMap 253 | */ 254 | 255 | const formatHelper = { 256 | validParts: function(/** @type {string} */ type) { 257 | if (type === 'standard') { 258 | return /t|hh?|HH?|p|P|z|ii?|ss?|dd?|DD?|S|mm?|MM?|yy(?:yy)?/g; 259 | } else if (type === 'php') { 260 | return /[dDjlNwzFmMnStyYaABgGhHisU]/g; 261 | } else { 262 | throw new Error('Invalid format type.'); 263 | } 264 | }, 265 | nonpunctuation: /[^ -\/:-@\[-`{-~\t\n\rTZ]+/g, 266 | /** 267 | * 268 | * @param {string} format 269 | * @param {string} type 270 | * @returns {{ separators: string[], parts: string[]} } 271 | */ 272 | parseFormat: function (/** @type {string} */ format, /** @type {string} */ type) { 273 | // IE treats \0 as a string end in inputs (truncating the value), 274 | // so it's a bad format delimiter, anyway 275 | var separators = format.replace(this.validParts(type), '\0').split('\0'), 276 | parts = format.match(this.validParts(type)) || []; 277 | if (!separators || !separators.length || !parts || parts.length === 0) { 278 | // throw new Error('Invalid date format.'); 279 | console.warn('invalid date format', separators, parts); 280 | } 281 | return {separators: separators, parts: parts}; 282 | }, 283 | /** 284 | * @param {string} type 285 | * @returns {{setters_map: SetterMap, setters_order: string[]}} 286 | */ 287 | setters: function(type) { 288 | /** @type {string[]} */ 289 | let setters_order 290 | /** @type {SetterMap} */ 291 | let setters_map = {}; 292 | if (type === 'standard') { 293 | setters_order = ['yyyy', 'yy', 'm', 'mm', 'M', 'MM','d', 'dd', 'D','DD', 'hh', 'h', 'HH', 'H', 'ii', 'i', 'ss', 's', 'S', 'p', 'P', 't']; 294 | setters_map = { 295 | hh: (d, v) => d.setHours(v), 296 | h: (d, v) => d.setHours(v), 297 | HH: (d, v) => d.setHours(v === 12 ? 0 : v), 298 | H: (d, v) => d.setHours(v === 12 ? 0 : v), 299 | i: (d, v) => d.setMinutes(v), 300 | s: (d, v) => d.setSeconds(v), 301 | yyyy: (d, v) => d.setFullYear(v), 302 | yy: (d, v) => d.setFullYear((v < 50 ? 2000 : 1900) + v), 303 | m: (d, v) => { 304 | v -= 1; 305 | while (v < 0) v += 12; 306 | v %= 12; 307 | d.setMonth(v); 308 | while (d.getMonth() !== v) 309 | if (isNaN(d.getMonth())) 310 | return d; 311 | else 312 | d.setDate(d.getDate() - 1); 313 | return d; 314 | }, 315 | d: (d, v) => d.setDate(v), 316 | p: (d, v) => d.setHours(v === 1 && d.getHours() < 12 ? d.getHours() + 12 : d.getHours()), 317 | t: (d, v) => d.setTime(v), 318 | mm: ()=>{}, 319 | M: ()=>{}, 320 | MM: ()=>{}, 321 | ii: ()=>{}, 322 | ss: ()=>{}, 323 | dd: ()=>{}, 324 | D: ()=>{}, 325 | DD: ()=>{}, 326 | P: ()=>{} 327 | }; 328 | setters_map.mm = setters_map.M = setters_map.MM = setters_map.m; 329 | setters_map.ii = setters_map.i; 330 | setters_map.ss = setters_map.s; 331 | setters_map.dd = setters_map.D = setters_map.DD = setters_map.d; 332 | setters_map.P = setters_map.p; 333 | } else { 334 | // php 335 | setters_order = ['Y','yy','m','M','F','n','d','D','j','l','N','S','H','G','h','g','i','s','p','P','U']; 336 | setters_map = { 337 | H: (d, v) => d.setHours(v), 338 | G: (d, v) => d.setHours(v), 339 | h: (d, v) => d.setHours(v === 12 ? 0 : v), 340 | g: (d, v) => d.setHours(v === 12 ? 0 : v), 341 | i: (d, v) => d.setMinutes(v), 342 | s: (d, v) => d.setSeconds(v), 343 | Y: (d, v) => d.setFullYear(v), 344 | yy: (d, v) => d.setFullYear((v < 50 ? 2000 : 1900) + v), 345 | m: (d, v) => { 346 | v -= 1; 347 | while (v < 0) v += 12; 348 | v %= 12; 349 | d.setMonth(v); 350 | while (d.getMonth() !== v) 351 | if (isNaN(d.getMonth())) 352 | return d; 353 | else 354 | d.setDate(d.getDate() - 1); 355 | return d; 356 | }, 357 | n: (d, v) => d.setMonth(v - 1), 358 | d: (d, v) => d.setDate(v), 359 | a: (d, v) => d.setHours(v === 1 ? d.getHours() + 12 : d.getHours()), 360 | U: (d, v) => d.setTime(v * 1000) 361 | }; 362 | setters_map.F = setters_map.M = setters_map.m; 363 | setters_map.D = setters_map.j = setters_map.l = setters_map.N = setters_map.d; 364 | setters_map.A = setters_map.a; 365 | } 366 | return { setters_order, setters_map }; 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/lib/utils/grid.js: -------------------------------------------------------------------------------- 1 | 2 | import { MODE_YEAR, MODE_DECADE } from "./constants.js"; 3 | import { getDaysInMonth } from "./dateUtils.js"; 4 | 5 | /** 6 | * @typedef {object} Dataset 7 | * @property {any[][]} grid 8 | * @property {Date[][]} days 9 | * @property {string[][]} months 10 | * @property {number[][]} years 11 | * @property {number[]} selectionMark 12 | * @property {number} todayMark 13 | * @property {number} prevTo 14 | * @property {number} nextFrom 15 | * 16 | * @typedef {import("$lib/i18n").i18nType} i18nType 17 | */ 18 | 19 | /** 20 | * Compute view grid content based on given 'currentView' property 21 | * 22 | * @param {Date} currentDate 23 | * @param {Date[]} selectedDates 24 | * @param {number} view 25 | * @param {i18nType} locale 26 | * @param {number} weekStart 27 | * @returns {Dataset} 28 | */ 29 | export function compute(currentDate, selectedDates, view, locale, weekStart) { 30 | 31 | /** ************************************ MODE_DECADE: */ 32 | /** ************************************ years 4 x 3 */ 33 | if (view === MODE_DECADE) { 34 | let prevTo = 10; // base is year 2000 35 | let nextFrom = 20; 36 | const todayMark = -1; 37 | const grid = []; 38 | let yearRow = []; 39 | let currYear = currentDate.getFullYear() - (currentDate.getFullYear() % 10); 40 | currYear -= (currYear % 20 ? 12 : 10); 41 | if (currYear % 10) { // if start is 10 42 | prevTo = 12; 43 | nextFrom = 22; 44 | } 45 | for (let i = 0; i < 32; i++) { 46 | yearRow.push(currYear + i); 47 | if (yearRow.length === 4) { 48 | grid.push(yearRow); 49 | yearRow = []; 50 | } 51 | } 52 | /** @var number[] */ 53 | let selectionMark = []; 54 | if (!selectedDates[0]) { 55 | selectedDates[0] = new Date(); 56 | } 57 | if (selectedDates[0].getFullYear() >= currYear) { 58 | selectionMark.push(selectedDates[0].getFullYear() % currYear); 59 | } 60 | 61 | // @ts-ignore 62 | return { 63 | years: grid, todayMark, nextFrom, prevTo, selectionMark 64 | } 65 | } 66 | 67 | /** ************************************ MODE_YEAR: */ 68 | /** ************************************ months 4 x 3 */ 69 | if (view === MODE_YEAR) { 70 | let grid = []; 71 | let monthRow = []; 72 | let prevTo = 12; 73 | let nextFrom = 24; 74 | const ISO = currentDate.toISOString().split('T')[0].substring(0, 8); 75 | const dateNormalized = new Date(ISO + '01 00:00:00'); 76 | const initYear = dateNormalized.getFullYear() - 1; 77 | dateNormalized.setFullYear(initYear); 78 | let todayMark = 0; 79 | for (let y = 0; y < 3; y++) { 80 | for (let i = 0; i < 12; i++) { 81 | dateNormalized.setMonth(i); 82 | monthRow.push(locale.monthsShort[i % 12]); 83 | if (monthRow.length === 4) { 84 | grid.push(monthRow); 85 | monthRow = []; 86 | } 87 | } 88 | dateNormalized.setFullYear(dateNormalized.getFullYear() + 1); 89 | } 90 | /** @type {number[]} */ 91 | let selectionMark = []; 92 | if (!selectedDates[0]) { 93 | selectedDates[0] = new Date(); 94 | } 95 | if (selectedDates[0].getFullYear() - initYear >= 0 && selectedDates[0].getFullYear() - initYear <= 2) { 96 | selectionMark.push(selectedDates[0].getMonth() + ((selectedDates[0].getFullYear() - initYear || 0) * 12)); 97 | } 98 | // @ts-ignore 99 | return { 100 | months: grid, todayMark, nextFrom, prevTo, selectionMark 101 | } 102 | } 103 | 104 | /** ************************************ MONTH */ 105 | /** ************************************ days 7x6 */ 106 | let d = currentDate || new Date(), // or currently selected date 107 | y = d.getFullYear(), 108 | m = d.getMonth(), 109 | dM = d.getDate(), 110 | h = d.getHours(), 111 | today = new Date(); 112 | let prevMonth = new Date(y, m-1, 28, 0, 0, 0, 0), 113 | day = getDaysInMonth(prevMonth.getFullYear(), prevMonth.getMonth()); 114 | prevMonth.setDate(day); 115 | prevMonth.setDate(day - (prevMonth.getDay() - weekStart + 7) % 7); 116 | 117 | let nextMonth = new Date(prevMonth); 118 | nextMonth.setDate(nextMonth.getDate() + 42); 119 | let nextMonthValue = nextMonth.valueOf(); 120 | 121 | let grid = []; 122 | let dayRow = []; 123 | let todayMark = -1; 124 | /** @type {number[]} */ 125 | let selectionMark = []; 126 | let prevTo = 0; 127 | let nextFrom = 42; 128 | let inc = 0; 129 | while(prevMonth.valueOf() < nextMonthValue) { 130 | inc++; 131 | dayRow.push(new Date(prevMonth)); 132 | if (prevMonth.getFullYear() < y || (prevMonth.getFullYear() === y && prevMonth.getMonth() < m)) { 133 | prevTo = inc; 134 | } else if (nextFrom === 42 && (prevMonth.getFullYear() > y || (prevMonth.getFullYear() === y && prevMonth.getMonth() > m))) { 135 | nextFrom = inc - 1; 136 | } 137 | 138 | prevMonth.setDate(prevMonth.getDate() + 1); 139 | 140 | 141 | if (prevMonth.getFullYear() === today.getFullYear() && 142 | prevMonth.getMonth() === today.getMonth() && 143 | prevMonth.getDate() === today.getDate() 144 | ) { 145 | todayMark = inc; 146 | } 147 | if (selectionMark.length !== selectedDates.length) { 148 | selectedDates.map(s => { 149 | if (prevMonth.getFullYear() === s.getFullYear() 150 | && prevMonth.getMonth() === s.getMonth() 151 | && prevMonth.getDate() === s.getDate() 152 | ) 153 | selectionMark.push(inc); 154 | }) 155 | } 156 | 157 | if (dayRow.length === 7) { 158 | grid.push(dayRow); 159 | dayRow = []; 160 | } 161 | } 162 | // @ts-ignore 163 | return { 164 | grid, days: grid, todayMark, prevTo, nextFrom, selectionMark, 165 | }; 166 | } 167 | -------------------------------------------------------------------------------- /src/lib/utils/state.js: -------------------------------------------------------------------------------- 1 | import { formatDate, parseDate } from "./dateUtils.js"; 2 | 3 | /** 4 | * @typedef {object} ValueInit 5 | * @property {string[]} iValues 6 | * @property {string|null} iValueCombined 7 | * @property {Date[]} iDates 8 | */ 9 | 10 | /** 11 | * Init internal props 12 | * 13 | * @param {string|string[]|null} value 14 | * @param {string} format 15 | * @param {import("$lib/i18n/index.js").i18nType} i18n 16 | * @param {string} formatType 17 | * @returns {ValueInit} 18 | */ 19 | export function initProps(value, format, i18n, formatType) { 20 | /** @type {string[]} */ 21 | let valueArray = value 22 | ? (Array.isArray(value) 23 | ? value 24 | : value.split(',') // for range we can get combined string 25 | ) 26 | : []; 27 | 28 | // strip seconds if present in initial value 29 | valueArray = valueArray.map(value => value.replace(/(:\d+):\d+(.*)/, "$1$2")); 30 | 31 | return { 32 | iValues: valueArray, 33 | iValueCombined: valueArray.length ? valueArray.join() : null, 34 | iDates: valueArray.map(val => parseDate(val, format, i18n, formatType)), 35 | } 36 | } 37 | 38 | /** 39 | * FUTURE: test that this works for PHP format type as well 40 | * 41 | * @param {'auto'|'date'|'datetime'|'time'} mode 42 | * @param {string} format 43 | * @returns {'date'|'datetime'|'time'} 44 | */ 45 | export function computeResolvedMode(mode, format) { 46 | return mode === "auto" 47 | ? format.match(/g|hh?|ii?/i) && format.match(/y|m|d/i) 48 | ? "datetime" 49 | : format.match(/g|hh?|ii?/i) 50 | ? "time" 51 | : "date" 52 | : mode; 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/utils/transitions.js: -------------------------------------------------------------------------------- 1 | import { cubicOut } from 'svelte/easing'; 2 | 3 | 4 | /** 5 | * 6 | * @typedef {object} ScaleParams 7 | * @property {number} duration 8 | * @property {number} start 9 | * @property {number} opacity 10 | * @property {number|undefined} end 11 | * 12 | * @param {HTMLElement} node 13 | * @param {ScaleParams | import('svelte/transition').FadeParams} params 14 | * @returns {import('svelte/transition').TransitionConfig} 15 | */ 16 | // @ts-ignore 17 | export function scale(node, { duration = 400, start = 0, end = 1, opacity = 0 }) { 18 | const style = getComputedStyle(node); 19 | const target_opacity = +style.opacity; 20 | const transform = style.transform === 'none' ? '' : style.transform; 21 | const sd = 1 - start; 22 | const od = target_opacity * (1 - opacity); 23 | end = end || 1; 24 | return { 25 | delay: 0, 26 | duration, 27 | easing: cubicOut, 28 | css: (/** @type number */ _t, /** @type number */ u) => ` 29 | transform: ${transform} scale(${end !== 1 ? start + end * u : 1 - (sd * u)}); 30 | opacity: ${target_opacity - (od * u)}; 31 | ` 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/routes/+layout.server.js: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 95 | 96 |
97 |
98 | {#if !isWide} 99 |
100 |
101 | 113 |
114 |
115 | {/if} 116 | 117 | Svelty Picker 118 | 119 |
120 | 121 | 122 | 123 | 127 | 128 |
129 |
130 |
131 | 132 |
133 |
134 |
135 | 158 |
159 |
160 | {@render children?.()} 161 |
162 | 173 |
174 |
175 |
176 |
177 | Made with Svelte ❤ by Martin Skocik.
178 | ♦
179 | You can support me through GitHub. 180 |
181 |
182 |
183 | 184 | 288 | -------------------------------------------------------------------------------- /src/routes/+page.svelte.md: -------------------------------------------------------------------------------- 1 | 42 | 43 | 44 | Simple date & time / daterange picker implemented in svelte. 45 | 46 |
47 | 48 |
49 |
50 | Date Picker: {date_datepicker} 51 |
52 | updatePickersValue(date, 'date')} /> 53 |
54 |
55 | Time Picker: {date_timepicker} 56 |
57 | updatePickersValue(time, 'time')} /> 58 |
59 |
60 |
61 | 62 | ### Features 63 | 64 | - date/time/datetime/range picker mode 65 | - various formatting options 66 | - keyboard navigation 67 | - replacable slots 68 | - themable 69 | - customizable disabled dates 70 | - custom element 71 | 72 | ## Install 73 | 74 | ```bash 75 | npm install svelty-picker 76 | ``` 77 | 78 | For Svelte 4 79 | 80 | ```bash 81 | npm install svelty-picker@5.2.11 82 | ``` 83 | 84 | ## More Examples 85 | 86 | 87 |
88 | 89 | Date Time Picker: {date_datetime} 90 | 91 |
92 | 93 |
94 |
95 | 96 |
97 | 98 |
99 | Date Range picker: {date_range} 100 | 101 |
102 | 103 |
104 |
105 | 106 | 107 |
108 | Date Time Range picker: {datetime_range} 109 | 110 |
111 | 112 |
113 |
114 | 115 | 139 | -------------------------------------------------------------------------------- /src/routes/autocommit-changes/+page.svelte.md: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | # Auto-commit changes 34 | 35 | 36 | On every change by mouse/touch, change callbacks are triggered. So if you use `datetime` picker, for every part - day, hour, minute there will be one event triggered. In some situations this may not be desired, therefore you can disable `autocommit` 37 | 38 | 39 | 40 |
41 | 42 |
43 | Historical context 44 |
45 |

46 | Previous versions (below v4) had problem that every interaction triggered change event. This was visible especially when picker was used in `datetime` mode. 47 | Therefore the whole logic has been reworked and basically we can talk about two modes - "auto commit" and "manual commit". 48 |

49 | 50 |

51 | In v6 it was reverted back, because it simplified codebase massively and it's really straightforward 52 |

53 |
54 |
55 | 56 |
57 | 58 | 59 | ### Auto-commit mode 60 | 61 | For datepicker it's simple. User picks a date, bound value (if any) is updated, `change` event triggered and picker is closed. 62 | For timepicker it's very similar (`change` triggered on every update) with the exception that picker closes automatically only 63 | after _minute_ selection. So you can change hour many times and picker will stay open. Select minutes and it will close. Also event is triggered etc. 64 | 65 | When using keyboard to change date, bound value is not updated immediately, but only by hitting Enter or on input 66 | blur. This is very important distinction against manual value setting. 67 | 68 | ### Manual-commit mode 69 | 70 | When `autocommit` is set to `false`, buttons **_Ok_** and **_Cancel_** are shown by default (can be overwritten by `actionRow` snippet). 71 | In this mode the main difference is, that value is set and event triggered only on `Enter` key or pressing `Ok` button. 72 | Pressing `Cancel` or leaving input _resets_ internal state to previous value (if set). 73 | 74 | ** 💡 IMPORTANT NOTE** 75 | 76 | When using `isRange` with combination of `'datetime` mode., auto-commit is _always_ OFF automatically to provide meaningful user experience. 77 | 78 | 79 | ## Example 80 | 81 |
82 |
83 | Settings 84 |
88 | 92 |
93 |
94 | Modes 95 | {#each modes as mode} 96 | 97 | {/each} 98 |
99 |
100 | 101 |
102 | {#if isRange} 103 | 104 | Selected value: {rangeValue} 105 | {:else} 106 | {#key activeMode} 107 | 108 | {/key} 109 | Selected value: {value} 110 | {/if} 111 |
112 | 113 | 121 | -------------------------------------------------------------------------------- /src/routes/disabling-dates/+page.svelte.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | # Disabling dates 14 | 15 | Restricting selectable is supported by `startDate` and `endDate` properties. You can provide `string` or `Date`. 16 | When `string` is passed, it must match `format` of given Svelty-picker. These border dates are *INCLUDED* in allowed date range. 17 | 18 | It is recommended to pass `strings` and not `Dates` to these properties. When datetime is passed, time range will be restricted 19 | as well if applicable. If conversion from `Date` to `string` is required, you can use ulitity functions as mentioned on [Working with dates](/working-with-dates) page. 20 | 21 | If you need something advanced you can provide function through `disableDatesFn`, in which you can resolve whether given date is disabled or not. 22 | Just take into account that all `Date` objects passed into this function will be in local time of given user. 23 | 24 | ```js 25 | function isDateDisabled(date: Date): bool 26 | ``` 27 | 28 | Both methods can be combined together if needed. 29 | 30 | ### Example 31 | 32 |
33 | 34 | 35 | 36 | 37 | ```svelte 38 | 48 | 49 | 50 | 51 | 52 | 53 | ``` 54 | 55 | 60 | -------------------------------------------------------------------------------- /src/routes/global-config/+page.svelte.md: -------------------------------------------------------------------------------- 1 | # Global config 2 | 3 | Svelty picker exports `config` property, where you can set/change some of the properties globally, which will affect all newly created instances. 4 | 5 | Can be usefull especially for formatting, i18n and theme setting to be consistent across the whole app. You should edit the config on your app start to reflect new settings for all _newly created_ pickers. 6 | 7 | ### Structure 8 | 9 | ```js 10 | type Config = { 11 | theme: string; 12 | format: string; 13 | formatType: string; 14 | displayFormat: string?; 15 | displayFormatType: string?; 16 | minuteIncrement: number; 17 | weekStart: number; 18 | inputClasses: string; 19 | todayBtnClasses: string; 20 | clearBtnClasses: string; 21 | hourOnly: boolean; 22 | todayBtn: boolean; 23 | clearBtn: boolean; 24 | autocommit: boolean; 25 | i18n: i18nType; 26 | } 27 | ``` 28 | 29 | ### Defaults 30 | 31 | ```js 32 | export default { 33 | theme: 'sdt-calendar-colors', 34 | format: 'yyyy-mm-dd', 35 | formatType: 'standard', 36 | displayFormat: null, 37 | displayFormatType: null, 38 | minuteIncrement: 1, 39 | weekStart: 1, 40 | inputClasses: '', 41 | todayBtnClasses: 'sdt-action-btn sdt-today-btn', 42 | clearBtnClasses: 'sdt-action-btn sdt-clear-btn', 43 | hourOnly: false, 44 | todayBtn: true, 45 | clearBtn: true, 46 | autocommit: true, 47 | i18n: en 48 | } 49 | ``` 50 | 51 | ### Example 52 | 53 | ```svelte 54 | 62 | 63 |
64 |
65 |

My german picker (global)

66 | 67 |
68 |
69 |

Japanese override

70 | 71 |
72 |
73 | ``` 74 | -------------------------------------------------------------------------------- /src/routes/localization/+page.svelte.md: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | # Localization 11 | 12 | By default date picker uses english locale. And at the moment there is only few locales available 13 | (PRs for additional locales are more than welcome). 14 | 15 | Available locales: {keys} 16 | 17 | Setting locale is perfect use case for using [global configuration](/global-config). 18 | 19 | ## Example 20 | 21 |
22 |
23 |
My german picker
24 | 25 |
26 |
27 |
Japanese override
28 | 29 |
30 |
31 | 32 |
33 | 34 |
35 | 36 | 37 | ```svelte 38 | 46 | 47 |
48 |
49 |

My german picker

50 | 51 |
52 |
53 |

Japanese override

54 | 55 |
56 |
57 | ``` 58 | 59 | 65 | -------------------------------------------------------------------------------- /src/routes/migration-guide/+page.svelte.md: -------------------------------------------------------------------------------- 1 | # Migration from v5 2 | 3 | Version 6 requires at least `svelte@5`. 4 | 5 | Main point of version 6 was to align with svelte 5. So deprecated features in svelte 5 were removed completely and replaced accordingly. 6 | Therefore amount of BC breaks is minimal. 7 | 8 | ## Slots 9 | 10 | Slots were removed in favour of snippets. Slot `inputs` has been removed completely with no replacement. New snippet 11 | `actionRow` has extended parameters to be fully compatible with default implementation. See [Snippets](/snippets) for 12 | more details. 13 | 14 | ## Properties 15 | 16 | ### `initialDate` 17 | 18 | Since svelte 5.9 where function bindings became available, this property has no meaning. Function bindings are more 19 | powerful, allowing things like binding to `Date` object directly and handle timezones as well. 20 | 21 | ```svelte 22 | // [!code --] 23 | // [!code ++] 24 | ``` 25 | 26 | [Function bindings example](https://svelte.dev/playground/1ddd82f573b94201b3c8fcab33bf0a46?version=5.9.0) 27 | 28 | ## Events 29 | 30 | Due to deprecation of `createEventDispatcher` all events has been replaced by event callback properties. 31 | 32 | ```svelte 33 | // [!code --] 34 | // [!code ++] 35 | ``` 36 | This is the event to prop mapping list: 37 | 38 | - `change` changed to `onChange` 39 | - `dateChange` changed to `onDateChange` 40 | - `focus` changed to `onBlur` 41 | - `input` changed to `onInput` 42 | - `blur` changed to `onBlur` 43 | 44 | Visit [Properties](/properties#event-callback-props) for more details. 45 | 46 | ## Event dispatching 47 | 48 | During migration to rune mode logic behind triggering event callback properties has been reworked completely and _regressed_ 49 | back as it was before version 4. 50 | 51 | What it means is that if you have `datetime` picker, `onChange` callback will be called 3 times, once on `date`, `hour` and `minute` event. 52 | If you need to check for specific event, you have 2 options: 53 | 54 | - use `onDateChange` event callback. It passes which event `type` triggered the callback 55 | - use picker with `autocommit=false` 56 | 57 | ## CSS update 58 | 59 | Last occurrences of `std-` class prefix fixed, namely `std-component-wrap` and `std-calendar-wrap` has been renamed. 60 | -------------------------------------------------------------------------------- /src/routes/modes-and-formats/+page.svelte.md: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | # Modes & formats 25 | 26 | 27 | {#snippet standard()} 28 | | Format | Description | Example 29 | |--------|----------------------------------------------------------------------------------|-------------------------------------- 30 | | `d` | day of the month without leading zeros | 1 to 31 31 | | `dd` | day of the month, 2 digits with leading zeros | 01 to 31 32 | | `D` | short textual representation of a weekday (i18n.daysShort) | Mon through Sun 33 | | `DD` | long textual representation of a weekday (i18n.days) | Sunday through Saturday 34 | | `S` | English ordinal suffix for the day of the month, (i18n.suffix) | st, nd, rd or th. Works well with `d` 35 | | `m` | numeric representation of month without leading zeros | 1 to 12 36 | | `mm` | numeric representation of the month, 2 digits with leading zeros | 01 to 12 37 | | `M` | short textual representation of a month, three letters (i18n.monthsShort) | Jan through Dec 38 | | `MM` | full textual representation of a month, such as January or March (i18n.months) | January through December 39 | | `yy` | two digit representation of a year | 99 or 03 40 | | `yyyy` | full numeric representation of a year, 4 digits | 1999, 2003 41 | | `h` | hour without leading zeros - 24-hour format | 0 - 23 42 | | `hh` | hour, 2 digits with leading zeros - 24-hour format | 00 - 23 43 | | `H` | hour without leading zeros - 12-hour format | 1 - 12 44 | | `HH` | hour, 2 digits with leading zeros - 12-hour format | 01 - 12 45 | | `i` | minutes, 2 digits with leading zeros | 00 - 59 46 | | `ii` | alias for `i` | 00 - 59 47 | | `s` | seconds, 2 digits with leading zeros | 00 48 | | `ss` | alias for `s` | 00 49 | | `p` | meridian in lower case ('am' or 'pm') - according to locale file (i18n.meridiem) | am or pm 50 | | `P` | meridian in upper case ('AM' or 'PM') - according to locale file (i18n.meridiem) | AM or PM 51 | | `t` | timestamp in milliseconds (although milliseconds are always 0). | 52 | {/snippet} 53 | 54 | {#snippet php()} 55 | | Format | Description | Example 56 | |------|---------------------------------------------------------------------|-------------------------------------- 57 | | `d` | Day of the month, 2 digits with leading zeros | 01 to 31 58 | | `D` | A textual representation of a day, three letters | Mon through Sun 59 | | `j` | Day of the month without leading zeros | 1 to 31 60 | | `l` | A full textual representation of the day of the week | Sunday through Saturday 61 | | `N` | ISO 8601 numeric representation of the day of the week | 1 (for Monday) through 7 (for Sunday) 62 | | `S` | English ordinal suffix for the day of the month, 2 characters | st, nd, rd or th. Works well with `j` 63 | | `F` | A full textual representation of a month, such as January or March | January through December 64 | | `m` | Numeric representation of a month, with leading zeros | 01 through 12 65 | | `M` | A short textual representation of a month, three letters | Jan through Dec 66 | | `n` | Numeric representation of a month, without leading zeros | 1 through 12 67 | | `Y` | A full numeric representation of a year, at least 4 digits | 0787, 1999, 2003 68 | | `y` | A two digit representation of a year | 99 or 03 69 | | `a` | Lowercase Ante meridiem and Post meridiem | am or pm 70 | | `A` | Uppercase Ante meridiem and Post meridiem | AM or PM 71 | | `g` | 12-hour format of an hour without leading zeros | 1 through 12 72 | | `G` | 24-hour format of an hour without leading zeros | 0 through 23 73 | | `h` | 12-hour format of an hour with leading zeros | 01 through 12 74 | | `H` | 24-hour format of an hour with leading zeros | 00 through 23 75 | | `i` | Minutes with leading zeros | 00 to 59 76 | | `s` | Seconds with leading zeros | 00 77 | | `U` | timestamp in seconds. | 78 | {/snippet} 79 | 80 | ### Mode 81 | 82 | By `mode` property you can restrict modes in which picker operates. This property can have following values: 83 | 84 | - `date` - only date picker 85 | - `time` - only time picker 86 | - `datetime` - date & time picker 87 | - `auto` (default) - mode is determined based on passed `format` property. This basically means you can activate `time` picker mode by setting `displayFormat` (or `format` if those 2 values should be the same) to `hh:ii` for example 88 | 89 | *Note:* range-picker is activated by `isRange` property, so you can still set appropriate mode for range-pickers. 90 | 91 | ### Format 92 | 93 | Component has 2 separate format-related properties: 94 | 95 | - `format` - defines string representation of selected Date object(s). This string is sent in `change` event or when form is submitted. Default value is `yyyy-mm-dd` 96 | - `displayFormat` - independent date format controlling how the date is being displayed to the user. When not set, `format` value is being used. 97 | 98 | Both propertie also have corresponding _type_ property, ie. `formatType` for `format` and `displayFormatType` for `displayFormat`. 99 | There are 2 available options for `formatType` props - `standard` (default) and `php`. And again if `displayFormatType` is undefined, `formatType` is being used as fallback. 100 | 101 |
102 | 103 |
104 | 105 | 💡 For timestamp in ***seconds*** use `php` formatting. For timestamp with ***miliseconds*** use `standard` formatting. 106 | 107 | ### Example 108 | 109 |
110 |
111 |
112 | Format === displayFormat
Internal value: {valueDefault || 'null'} 113 |
114 | 115 |
116 |
117 |
118 | Format !== displayFormat
Internal value: {valueDifferent || 'null'} 119 |
120 | 121 |
122 |
123 |
124 | Time only format:
Internal value: {valueTimeOnly || 'null'} 125 |
126 | 127 |
128 |
129 | 130 | ```svelte 131 |
132 |
133 |
134 | Format === displayFormat
Internal value: {valueDefault} 135 |
136 | 137 |
138 |
139 |
140 | Format !== displayFormat
Internal value: {valueDifferent} 141 |
142 | 143 |
144 |
145 |
146 | Time only format:
Internal value: {valueTimeOnly} 147 |
148 | 149 |
150 |
151 | ``` 152 | 153 | 159 | -------------------------------------------------------------------------------- /src/routes/modes-and-formats/Tabs.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
    13 | {#each items as item} 14 |
  • 15 | 16 |
  • 17 | {/each} 18 |
19 | {#each items as item} 20 | {#if activeTabValue == item.value} 21 |
22 | 23 |
24 | {/if} 25 | {/each} 26 | 27 | 69 | -------------------------------------------------------------------------------- /src/routes/modes-and-formats/format-php.md: -------------------------------------------------------------------------------- 1 | | Format | Description | Example 2 | |------|---------------------------------------------------------------------|-------------------------------------- 3 | | `d` | Day of the month, 2 digits with leading zeros | 01 to 31 4 | | `D` | A textual representation of a day, three letters | Mon through Sun 5 | | `j` | Day of the month without leading zeros | 1 to 31 6 | | `l` | A full textual representation of the day of the week | Sunday through Saturday 7 | | `N` | ISO 8601 numeric representation of the day of the week | 1 (for Monday) through 7 (for Sunday) 8 | | `S` | English ordinal suffix for the day of the month, 2 characters | st, nd, rd or th. Works well with `j` 9 | | `F` | A full textual representation of a month, such as January or March | January through December 10 | | `m` | Numeric representation of a month, with leading zeros | 01 through 12 11 | | `M` | A short textual representation of a month, three letters | Jan through Dec 12 | | `n` | Numeric representation of a month, without leading zeros | 1 through 12 13 | | `Y` | A full numeric representation of a year, at least 4 digits | 0787, 1999, 2003 14 | | `y` | A two digit representation of a year | 99 or 03 15 | | `a` | Lowercase Ante meridiem and Post meridiem | am or pm 16 | | `A` | Uppercase Ante meridiem and Post meridiem | AM or PM 17 | | `g` | 12-hour format of an hour without leading zeros | 1 through 12 18 | | `G` | 24-hour format of an hour without leading zeros | 0 through 23 19 | | `h` | 12-hour format of an hour with leading zeros | 01 through 12 20 | | `H` | 24-hour format of an hour with leading zeros | 00 through 23 21 | | `i` | Minutes with leading zeros | 00 to 59 22 | | `s` | Seconds with leading zeros | 00 23 | | `U` | timestamp in seconds. | 24 | 25 | 26 | ☝️ For timestamp with miliseconds use `standard` formatting. 27 | -------------------------------------------------------------------------------- /src/routes/modes-and-formats/format-standard.md: -------------------------------------------------------------------------------- 1 | | Format | Description | Example 2 | |--------|----------------------------------------------------------------------------------|-------------------------------------- 3 | | `d` | day of the month without leading zeros | 1 to 31 4 | | `dd` | day of the month, 2 digits with leading zeros | 01 to 31 5 | | `D` | short textual representation of a weekday (i18n.daysShort) | Mon through Sun 6 | | `DD` | long textual representation of a weekday (i18n.days) | Sunday through Saturday 7 | | `S` | English ordinal suffix for the day of the month, (i18n.suffix) | st, nd, rd or th. Works well with `d` 8 | | `m` | numeric representation of month without leading zeros | 1 to 12 9 | | `mm` | numeric representation of the month, 2 digits with leading zeros | 01 to 12 10 | | `M` | short textual representation of a month, three letters (i18n.monthsShort) | Jan through Dec 11 | | `MM` | full textual representation of a month, such as January or March (i18n.months) | January through December 12 | | `yy` | two digit representation of a year | 99 or 03 13 | | `yyyy` | full numeric representation of a year, 4 digits | 1999, 2003 14 | | `h` | hour without leading zeros - 24-hour format | 0 - 23 15 | | `hh` | hour, 2 digits with leading zeros - 24-hour format | 00 - 23 16 | | `H` | hour without leading zeros - 12-hour format | 1 - 12 17 | | `HH` | hour, 2 digits with leading zeros - 12-hour format | 01 - 12 18 | | `i` | minutes, 2 digits with leading zeros | 00 - 59 19 | | `ii` | alias for `i` | 00 - 59 20 | | `s` | seconds, 2 digits with leading zeros | 00 21 | | `ss` | alias for `s` | 00 22 | | `p` | meridian in lower case ('am' or 'pm') - according to locale file (i18n.meridiem) | am or pm 23 | | `P` | meridian in upper case ('AM' or 'PM') - according to locale file (i18n.meridiem) | AM or PM 24 | | `t` | timestamp in milliseconds (although milliseconds are always 0). | 25 | 26 | 27 | ☝️ For timestamp in seconds use `php` formatting. 28 | -------------------------------------------------------------------------------- /src/routes/properties/+page.svelte.md: -------------------------------------------------------------------------------- 1 | # Properties 2 | 3 | Scroll down for [event callback props](#event-callback-props). 4 | 5 |
6 | 7 | | Property | Type | Default | Description | 8 | |-----------------|-----------------------------|-------------------|-------------------| 9 | | inputId | `string` | `""` | id attribute for input element 10 | | name | `string` | `'date'` | html attribute for underlying `` element | 11 | | disabled | `bool` | `false` | html attribute for underlying `` element | 12 | | placeholder | `string` | `null` | html attribute for underlying `` element | 13 | | required | `bool` | `false` | html attribute for underlying `` element | 14 | | value | `string`\|`string[]`\|`null`| `null` | string representation of selected value. When in daterange mode (`isRange` prop), array of two strings is required | 15 | | isRange | `bool` | `false` | enables range picker mode | 16 | | startDate | string\|Date | `null` | limit minimal selectable date | 17 | | endDate | string\|Date | `null` | limit maximal selectable date | 18 | | pickerOnly | `bool` | `false` | Picker is always visible and input field is then hidden, but still present | 19 | | startView | `number` | `2` | Which mode should picker at, `0` - decade, `1` - year, `2` - month (default), `3` - time picker 20 | | mode | `string` | `auto` | restrict picker's mode. Possible values: `auto\|date\|datetime\|time`. By default it try to guess the mode from `format` | 21 | | disableDatesFn | `function` | `null` | Function whether passed date should be disabled or not | 22 | | manualInput | `bool` | `false` | Whether manual date entry is allowed | 23 | | format | `string` | `'yyyy-mm-dd'` | Format of entered date/time. | 24 | | formatType | `string` | `'standard'` | Format type (`standard` or `php`) | 25 | | displayFormat | `string` | `null` | Display format of entered date/time. | 26 | | displayFormatType | `string` | `null` | Display format type (`standard` or `php`) | 27 | | hourOnly | `bool` | `false` | Only allow hour selection for the time portion of the datetime selection 28 | | minuteIncrement | `number` | `1` | number in range `1-60` to set the increment of minutes choosable | 29 | | weekStart | `number` | `1` | number in range `0-6` to select first day of the week. Sunday is `0` | 30 | | inputClasses | `string` | `""` | input css class string | 31 | | todayBtnClasses | `string` | `'sdt-action-btn sdt-today-btn'` | today button css classes | 32 | | clearBtnClasses | `string` | `'sdt-action-btn sdt-clear-btn'` | clear button css classes | 33 | | todayBtn | `bool` | `true` | Show today button | 34 | | clearBtn | `bool` | `true` | Show clear button | 35 | | clearToggle | `bool` | `true` | Allows to clear selected date when clicking on the same date when in `mode='date'` or `mode='auto'` resolving to `'date'` | 36 | | autocommit | `bool` | `true` | Whether date/time selection is automatic or manual | 37 | | i18n | `object` | `en` | localization object, english is by default | 38 | | validatorAction | `array` | `null` | Bind validator action for inner `` element. Designed to be used with `svelte-use-form`. 39 | | positionResolver | `function` | internal | Action which resolves floating position of picker. Default one uses `@floating-ui` under the hood. So you can use this library for your custom position resolver function 40 | 41 |
42 | 43 | 44 | ## Event callback props 45 | 46 | 47 |
48 | 49 | | Property | Arguments | Description | 50 | |-----------------|-------------------------------|-------------- 51 | | `onChange` | `string`\|`string[]`\|`null` | Happens when value is changed. New value is passed as argument 52 | | `onDatechange` | `prop: onChangeProperty` | Happens when value is changed, but details passed on the event are more detailed, see the type below 53 | | `onFocus` | `` | When input is focused. Won't be trigger in `pickerOnly` mode 54 | | `onInput` | `string` | When manually entered input matches given `format`. `manualInput` must be set to allow value entering 55 | | `onBlur` | `` | When input loses focus. Won't be trigger in `pickerOnly` mode 56 | 57 |
58 | 59 | ```ts 60 | type onChangeProperty = { 61 | value: string|string[], 62 | dateValue: Date|Date[], 63 | displayValue: string, 64 | valueFormat: string, 65 | displayFormat: string, 66 | event: 'date'|'hour'|'minute'|'datetime' // which event triggered the callback 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /src/routes/snippets/+page.svelte.md: -------------------------------------------------------------------------------- 1 | 24 | 25 | # Snippets 26 | 27 | There are 2 available snippets to override/customize. 28 | 29 | 30 | ## children 31 | 32 | This snippet can be used for adding icon. Don't forget to position your icon absolutely. Or move your styling to ``'s parent element `.sdt-input-wrap`. 33 | 34 | Example of custom interactive svg icon showing currently selected date: 35 | 36 | 37 | 38 | 45 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Pick 72 | 73 | ? 82 | 83 | Date 92 | 93 | 94 | 95 | ```svelte 96 | 97 | 98 | 99 | ``` 100 | 101 | ## actionRow 102 | 103 | Snippet represents row below calendar/time picker. By default showing ***Today*** and ***Clear*** buttons. And if `autocommit`=`false` also 104 | ***Ok*** and ***Cancel*** buttons. 105 | 106 | 💡 If you just want to hide ***Today*** or ***Clear*** buttons, use controlling props and provide appropriate `i18n` message 107 | instead of overriding `actionRow` snippet. 108 | 109 | ### Snipet type 110 | 111 | ```ts 112 | Snippet<[ prop: { 113 | autocloseSupported: boolean, 114 | todayBtnClasses: string, 115 | clearBtnClasses: string, 116 | onCancel: function, 117 | onConfirm: function, 118 | onClear: function, 119 | onToday: function, 120 | isTodayDisabled: boolean, 121 | i18n: import('svelty-picker/i18n').i18nType, 122 | currentMode: string 123 | }]> 124 | ``` 125 | 126 | Parameter description: 127 | 128 | - `autocloseSupported` - determined by `autocommit` and `isRange` properties 129 | - `todayBtnClasses` - CSS classes for 'today' button 130 | - `clearBtnClasses` - CSS classes for 'clear' button 131 | - `onCancel` - discards current selection (when `autocommit` is set to `false`) 132 | - `onConfirm` - handler which confirms current selection (when `autocommit` is set to `false`) 133 | - `onClear` - clears current selection 134 | - `onToday` - selects today date 135 | - `isTodayDisabled` - useful when you use `startDate` or `endDate` or `disableDatesFn` to disable dates. Serves for disabled 'Today' button 136 | - `i18n` - current locale 137 | - `currentMode` - returns currently active mode: `date` or `time` (where `time` is available only for datetime or time picker) 138 | 139 | ### Default implementation 140 | 141 | ```svelte 142 | {#snippet action_row({ 143 | autocloseSupported, 144 | todayBtnClasses, 145 | clearBtnClasses, 146 | onCancel, 147 | onConfirm, 148 | onClear, 149 | onToday, 150 | isTodayDisabled, 151 | i18n, 152 | currentMode 153 | })} 154 | {#if !autocloseSupported || true} 155 |
156 | {#if !autocloseSupported} 157 | 158 | 165 | 172 | 173 | {/if} 174 | {#if todayBtn || clearBtn} 175 | 176 | {#if todayBtn && currentMode === 'date'} 177 | 185 | {/if} 186 | {#if clearBtn} 187 | 194 | {/if} 195 | 196 | {/if} 197 |
198 | {/if} 199 | {/snippet} 200 | ``` 201 | 202 | 223 | -------------------------------------------------------------------------------- /src/routes/theme/+page.svelte.md: -------------------------------------------------------------------------------- 1 | # Theme 2 | 3 | Component exposes following CSS variables to customize it's appearance as you can see through whole in example below or on the homepage. 4 | 5 | ```css 6 | :root { 7 | /* general */ 8 | --sdt-bg-main: #fff; /** wrap background color */ 9 | --sdt-shadow-color: #ccc; /** wrap shadow color */ 10 | --sdt-wrap-shadow: 0 1px 6px var(--sdt-shadow-color); /** wrap shadow settings */ 11 | --sdt-radius: 4px; /** wrap radius */ 12 | --sdt-color: #000; /** data to select(e.g date/time) text color (except header & buttons) */ 13 | --sdt-color-selected: #fff; /** selected data(e.g date/time) text color */ 14 | --sdt-header-color: #000; /** header items color (e.g. text & buttons) */ 15 | --sdt-header-btn-bg-hover: #dfdfdf; /** header items hover background color */ 16 | --sdt-bg-selected: #286090; /** selected data(e.g date/time) background color */ 17 | 18 | /* action buttons */ 19 | --sdt-today-bg: #1e486d; /** date picker today button hover background color */ 20 | --sdt-today-color: var(--sdt-bg-main); /** date picker today button text & border color */ 21 | --sdt-clear-color: #dc3545; /** clear button text & border color */ 22 | --sdt-clear-bg: transparent; /** clear button background color */ 23 | --sdt-clear-hover-color: var(--sdt-bg-main); /** clear button hover text color */ 24 | --sdt-clear-hover-bg: #dc3545; /** clear button hover background color */ 25 | 26 | /* time picker */ 27 | --sdt-clock-selected-bg: var(--sdt-bg-selected); /** selected time background color */ 28 | --sdt-clock-bg: #eeeded; /** time picker inner circle background color */ 29 | --sdt-clock-color: var(--sdt-color); /** time picker text color (watch "--sdt-color") */ 30 | --sdt-clock-color-hover: var(--sdt-color); /** time picker hover text color (watch "--sdt-color") */ 31 | --sdt-clock-time-bg: transparent; /** time picker time background color */ 32 | --sdt-clock-time-bg-hover: transparent; /** time picker time selection hover background color */ 33 | --sdt-clock-disabled-time: #b22222; /** disabled time picker time text color */ 34 | --sdt-clock-disabled-time-bg: #eee; /** disabled time picker time background color */ 35 | 36 | /* date picker */ 37 | --sdt-table-selected-bg: var(--sdt-bg-selected); /** selected date background color */ 38 | --sdt-table-disabled-date: #b22222; /** disabled dates text color */ 39 | --sdt-table-disabled-date-bg: #eee; /** disabled dates background color */ 40 | --sdt-table-bg: transparent; /** date picker inner table background color */ 41 | --sdt-table-data-bg-hover: #eee; /** table selection data hover background color */ 42 | --sdt-table-today-indicator: #ccc; /** date picker current day marker color */ 43 | 44 | } 45 | ``` 46 | 47 | ### Definitions from this website 48 | 49 | ```html 50 | 109 | ``` 110 | -------------------------------------------------------------------------------- /src/routes/working-with-dates/+page.svelte.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | # Working with dates 13 | 14 | Although library works internally `Date` objects, `value` property must be `string` or `string[]` when in daterange mode. 15 | It makes internal logic simpler and makes library dependency-free. 16 | 17 | Since svelte 5.9 you can use [function binding](https://svelte.dev/docs/svelte/bind#Function-bindings) for binding `value` property. 18 | Therefore you can convert your date objects to strings and vice versa. Even handle timezones! There are 2 helper functions 19 | available to help you with these transformations. Library use them internaly to transform to/from specified [formats](/modes-and-formats). 20 | 21 | 22 | ```js 23 | /** 24 | * String -> date 25 | * 26 | * @param {Date|string} date 27 | * @param {string} format 28 | * @param {i18nType} i18n 29 | * @param {string} type 30 | * @returns {Date} 31 | */ 32 | export function parseDate(date, format, i18n, type) { /* ... */} 33 | 34 | /** 35 | * Date -> string 36 | * 37 | * @param {Date} date 38 | * @param {string} format 39 | * @param {i18nType} i18n 40 | * @param {string} type 41 | * @returns {string} 42 | */ 43 | export function formatDate(date, format, i18n, type) { /* ... */} 44 | ``` 45 | 46 |
47 |
48 | Version 5 and below 49 |
50 |

51 | In v5 it was possible to set initial date through initialDate, which has been removed in v6. 52 |

53 |
54 |
55 |
56 | 57 | ### Example 58 | 59 | `date` variable value: {date} 60 | { 65 | return formatDate(date, 'yyyy/mm/dd', en, 'standard'); 66 | }, 67 | (v) => { 68 | date = v ? parseDate(v, 'yyyy/mm/dd', en, 'standard') : null; 69 | } 70 | } 71 | /> 72 | 73 | ```svelte 74 | 84 | 85 | Selected date: {date} 86 | { 91 | return formatDate(date, 'yyyy/mm/dd', en, 'standard'); 92 | }, 93 | (v) => { 94 | date = v ? parseDate(v, 'yyyy/mm/dd', en, 'standard') : null; 95 | } 96 | } 97 | /> 98 | ``` 99 | -------------------------------------------------------------------------------- /src/style/base.css: -------------------------------------------------------------------------------- 1 | @media (prefers-reduced-motion: reduce) { 2 | *, 3 | ::before, 4 | ::after { 5 | animation-delay: -1ms !important; 6 | animation-duration: 1ms !important; 7 | animation-iteration-count: 1 !important; 8 | background-attachment: initial !important; 9 | scroll-behavior: auto !important; 10 | transition-duration: 0s !important; 11 | transition-delay: 0s !important; 12 | } 13 | } 14 | 15 | *, 16 | ::before, 17 | ::after { 18 | box-sizing: border-box; 19 | } 20 | 21 | html { 22 | line-height: 1.4; 23 | font-size: 16px; 24 | -webkit-text-size-adjust: 100%; 25 | } 26 | 27 | html.dark { 28 | color-scheme: dark; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | width: 100%; 34 | min-width: 320px; 35 | min-height: 100vh; 36 | line-height: 24px; 37 | font-family: var(--vp-font-family-base); 38 | font-size: 16px; 39 | font-weight: 400; 40 | color: var(--vp-c-text-1); 41 | background-color: var(--vp-c-bg); 42 | font-synthesis: style; 43 | text-rendering: optimizeLegibility; 44 | -webkit-font-smoothing: antialiased; 45 | -moz-osx-font-smoothing: grayscale; 46 | } 47 | 48 | main { 49 | display: block; 50 | } 51 | 52 | h1, 53 | h2, 54 | h3, 55 | h4, 56 | h5, 57 | h6 { 58 | margin: 0; 59 | line-height: 24px; 60 | font-size: 16px; 61 | font-weight: 400; 62 | } 63 | 64 | p { 65 | margin: 0; 66 | } 67 | 68 | strong, 69 | b { 70 | font-weight: 600; 71 | } 72 | 73 | /** 74 | * Avoid 300ms click delay on touch devices that support the `touch-action` 75 | * CSS property. 76 | * 77 | * In particular, unlike most other browsers, IE11+Edge on Windows 10 on 78 | * touch devices and IE Mobile 10-11 DON'T remove the click delay when 79 | * `` is present. 80 | * However, they DO support removing the click delay via 81 | * `touch-action: manipulation`. 82 | * 83 | * See: 84 | * - http://v4-alpha.getbootstrap.com/content/reboot/#click-delay-optimization-for-touch 85 | * - http://caniuse.com/#feat=css-touch-action 86 | * - http://patrickhlauke.github.io/touch/tests/results/#suppressing-300ms-delay 87 | */ 88 | a, 89 | area, 90 | button, 91 | [role='button'], 92 | input, 93 | label, 94 | select, 95 | summary, 96 | textarea { 97 | touch-action: manipulation; 98 | } 99 | 100 | a { 101 | color: inherit; 102 | text-decoration: inherit; 103 | } 104 | 105 | ol, 106 | ul { 107 | list-style: none; 108 | margin: 0; 109 | padding: 0; 110 | } 111 | 112 | blockquote { 113 | margin: 0; 114 | } 115 | 116 | pre, 117 | code, 118 | kbd, 119 | samp { 120 | font-family: var(--vp-font-family-mono); 121 | } 122 | 123 | img, 124 | svg, 125 | video, 126 | canvas, 127 | audio, 128 | iframe, 129 | embed, 130 | object { 131 | display: block; 132 | } 133 | 134 | figure { 135 | margin: 0; 136 | } 137 | 138 | img, 139 | video { 140 | max-width: 100%; 141 | height: auto; 142 | } 143 | 144 | button, 145 | input, 146 | optgroup, 147 | select, 148 | textarea { 149 | border: 0; 150 | padding: 0; 151 | line-height: inherit; 152 | color: inherit; 153 | } 154 | 155 | button { 156 | padding: 0; 157 | font-family: inherit; 158 | background-color: transparent; 159 | background-image: none; 160 | } 161 | 162 | button:enabled, 163 | [role='button']:enabled { 164 | cursor: pointer; 165 | } 166 | 167 | button:focus, 168 | button:focus-visible { 169 | outline: 1px dotted; 170 | outline: 4px auto -webkit-focus-ring-color; 171 | } 172 | 173 | button:focus:not(:focus-visible) { 174 | outline: none !important; 175 | } 176 | 177 | input:focus, 178 | textarea:focus, 179 | select:focus { 180 | outline: none; 181 | } 182 | 183 | table { 184 | border-collapse: collapse; 185 | } 186 | 187 | input { 188 | background-color: transparent; 189 | } 190 | 191 | input:-ms-input-placeholder, 192 | textarea:-ms-input-placeholder { 193 | color: var(--vp-c-text-3); 194 | } 195 | 196 | input::-ms-input-placeholder, 197 | textarea::-ms-input-placeholder { 198 | color: var(--vp-c-text-3); 199 | } 200 | 201 | input::placeholder, 202 | textarea::placeholder { 203 | color: var(--vp-c-text-3); 204 | } 205 | 206 | input::-webkit-outer-spin-button, 207 | input::-webkit-inner-spin-button { 208 | -webkit-appearance: none; 209 | margin: 0; 210 | } 211 | 212 | input[type='number'] { 213 | -moz-appearance: textfield; 214 | } 215 | 216 | textarea { 217 | resize: vertical; 218 | } 219 | 220 | select { 221 | -webkit-appearance: none; 222 | } 223 | 224 | fieldset { 225 | margin: 0; 226 | padding: 0; 227 | } 228 | 229 | h1, 230 | h2, 231 | h3, 232 | h4, 233 | h5, 234 | h6, 235 | li, 236 | p { 237 | overflow-wrap: break-word; 238 | } 239 | 240 | vite-error-overlay { 241 | z-index: 9999; 242 | } 243 | 244 | mjx-container { 245 | display: inline-block; 246 | margin: auto 2px -2px; 247 | } 248 | 249 | mjx-container > svg { 250 | display: inline-block; 251 | margin: auto; 252 | } 253 | 254 | .app-wrap { 255 | min-height: 100vh; 256 | display: flex; 257 | } 258 | .page-container { 259 | display: flex; 260 | flex: 1; 261 | flex-direction: column; 262 | justify-content: space-between; 263 | } 264 | -------------------------------------------------------------------------------- /src/style/code.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Headings 3 | * -------------------------------------------------------------------------- */ 4 | 5 | .vp-doc h1, 6 | .vp-doc h2, 7 | .vp-doc h3, 8 | .vp-doc h4, 9 | .vp-doc h5, 10 | .vp-doc h6 { 11 | position: relative; 12 | font-weight: 600; 13 | outline: none; 14 | } 15 | 16 | .vp-doc h1 { 17 | letter-spacing: -0.02em; 18 | line-height: 40px; 19 | font-size: 28px; 20 | } 21 | 22 | .vp-doc h2 { 23 | margin: 48px 0 16px; 24 | border-top: 1px solid var(--vp-c-divider); 25 | padding-top: 24px; 26 | letter-spacing: -0.02em; 27 | line-height: 32px; 28 | font-size: 24px; 29 | } 30 | 31 | .vp-doc h3 { 32 | margin: 32px 0 0; 33 | letter-spacing: -0.01em; 34 | line-height: 28px; 35 | font-size: 20px; 36 | } 37 | 38 | .vp-doc .header-anchor { 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | margin-left: -0.87em; 43 | font-weight: 500; 44 | user-select: none; 45 | opacity: 0; 46 | text-decoration: none; 47 | transition: color 0.25s, opacity 0.25s; 48 | } 49 | 50 | .vp-doc .header-anchor:before { 51 | content: var(--vp-header-anchor-symbol); 52 | } 53 | 54 | .vp-doc h1:hover .header-anchor, 55 | .vp-doc h1 .header-anchor:focus, 56 | .vp-doc h2:hover .header-anchor, 57 | .vp-doc h2 .header-anchor:focus, 58 | .vp-doc h3:hover .header-anchor, 59 | .vp-doc h3 .header-anchor:focus, 60 | .vp-doc h4:hover .header-anchor, 61 | .vp-doc h4 .header-anchor:focus, 62 | .vp-doc h5:hover .header-anchor, 63 | .vp-doc h5 .header-anchor:focus, 64 | .vp-doc h6:hover .header-anchor, 65 | .vp-doc h6 .header-anchor:focus { 66 | opacity: 1; 67 | } 68 | 69 | @media (min-width: 768px) { 70 | .vp-doc h1 { 71 | letter-spacing: -0.02em; 72 | line-height: 40px; 73 | font-size: 32px; 74 | } 75 | } 76 | 77 | .vp-doc h2 .header-anchor { 78 | top: 24px; 79 | } 80 | 81 | /** 82 | * Paragraph and inline elements 83 | * -------------------------------------------------------------------------- */ 84 | 85 | .vp-doc p, 86 | .vp-doc summary { 87 | margin: 16px 0; 88 | } 89 | 90 | .vp-doc p { 91 | line-height: 28px; 92 | } 93 | 94 | .vp-doc blockquote { 95 | margin: 16px 0; 96 | border-left: 2px solid var(--vp-c-divider); 97 | padding-left: 16px; 98 | transition: border-color 0.5s; 99 | } 100 | 101 | .vp-doc blockquote > p { 102 | margin: 0; 103 | font-size: 16px; 104 | color: var(--vp-c-text-2); 105 | transition: color 0.5s; 106 | } 107 | 108 | .vp-doc a { 109 | font-weight: 500; 110 | color: var(--vp-c-brand-1); 111 | text-decoration: underline; 112 | text-underline-offset: 2px; 113 | transition: color 0.25s, opacity 0.25s; 114 | } 115 | 116 | .vp-doc a:hover { 117 | color: var(--vp-c-brand-2); 118 | } 119 | 120 | .vp-doc strong { 121 | font-weight: 600; 122 | } 123 | 124 | /** 125 | * Lists 126 | * -------------------------------------------------------------------------- */ 127 | 128 | .vp-doc ul, 129 | .vp-doc ol { 130 | padding-left: 1.25rem; 131 | margin: 16px 0; 132 | } 133 | 134 | .vp-doc ul { 135 | list-style: disc; 136 | } 137 | 138 | .vp-doc ol { 139 | list-style: decimal; 140 | } 141 | 142 | .vp-doc li + li { 143 | margin-top: 8px; 144 | } 145 | 146 | .vp-doc li > ol, 147 | .vp-doc li > ul { 148 | margin: 8px 0 0; 149 | } 150 | 151 | /** 152 | * Table 153 | * -------------------------------------------------------------------------- */ 154 | 155 | .vp-doc table { 156 | display: block; 157 | border-collapse: collapse; 158 | margin: 20px 0; 159 | overflow-x: auto; 160 | } 161 | 162 | .vp-doc tr { 163 | background-color: var(--vp-c-bg); 164 | border-top: 1px solid var(--vp-c-divider); 165 | transition: background-color 0.5s; 166 | } 167 | 168 | .vp-doc tr:nth-child(2n) { 169 | background-color: var(--vp-c-bg-soft); 170 | } 171 | 172 | .vp-doc th, 173 | .vp-doc td { 174 | border: 1px solid var(--vp-c-divider); 175 | padding: 8px 16px; 176 | } 177 | 178 | .vp-doc th { 179 | text-align: left; 180 | font-size: 14px; 181 | font-weight: 600; 182 | color: var(--vp-c-text-2); 183 | background-color: var(--vp-c-bg-soft); 184 | } 185 | 186 | .vp-doc td { 187 | font-size: 14px; 188 | } 189 | 190 | /** 191 | * Decorational elements 192 | * -------------------------------------------------------------------------- */ 193 | 194 | .vp-doc hr { 195 | margin: 16px 0; 196 | border: none; 197 | border-top: 1px solid var(--vp-c-divider); 198 | } 199 | 200 | /** 201 | * Custom Block 202 | * -------------------------------------------------------------------------- */ 203 | 204 | .vp-doc .custom-block { 205 | margin: 16px 0; 206 | } 207 | 208 | .vp-doc .custom-block p { 209 | margin: 8px 0; 210 | line-height: 24px; 211 | } 212 | 213 | .vp-doc .custom-block p:first-child { 214 | margin: 0; 215 | } 216 | 217 | .vp-doc .custom-block div[class*="language-"] { 218 | margin: 8px 0; 219 | border-radius: 8px; 220 | } 221 | 222 | .vp-doc .custom-block div[class*="language-"] code { 223 | font-weight: 400; 224 | background-color: transparent; 225 | } 226 | 227 | .vp-doc .custom-block .vp-code-group .tabs { 228 | margin: 0; 229 | border-radius: 8px 8px 0 0; 230 | } 231 | 232 | /** 233 | * Code 234 | * -------------------------------------------------------------------------- */ 235 | 236 | /* inline code */ 237 | .vp-doc :not(pre, h1, h2, h3, h4, h5, h6) > code { 238 | font-size: var(--vp-code-font-size); 239 | color: var(--vp-code-color); 240 | } 241 | 242 | .vp-doc :not(pre) > code { 243 | border-radius: 4px; 244 | padding: 3px 6px; 245 | background-color: var(--vp-code-bg); 246 | transition: color 0.25s, background-color 0.5s; 247 | } 248 | 249 | .vp-doc a > code { 250 | color: var(--vp-code-link-color); 251 | } 252 | 253 | .vp-doc a:hover > code { 254 | color: var(--vp-code-link-hover-color); 255 | } 256 | 257 | .vp-doc h1 > code, 258 | .vp-doc h2 > code, 259 | .vp-doc h3 > code { 260 | font-size: 0.9em; 261 | } 262 | 263 | .vp-doc div[class*="language-"], 264 | .vp-block { 265 | position: relative; 266 | margin: 16px -24px; 267 | background-color: var(--vp-code-block-bg); 268 | overflow-x: auto; 269 | transition: background-color 0.5s; 270 | } 271 | 272 | @media (min-width: 640px) { 273 | .vp-doc div[class*="language-"], 274 | .vp-block { 275 | border-radius: 8px; 276 | margin: 16px 0; 277 | } 278 | } 279 | 280 | @media (max-width: 639px) { 281 | .vp-doc li div[class*="language-"] { 282 | border-radius: 8px 0 0 8px; 283 | } 284 | } 285 | 286 | .vp-doc div[class*="language-"] + div[class*="language-"], 287 | .vp-doc div[class$="-api"] + div[class*="language-"], 288 | .vp-doc div[class*="language-"] + div[class$="-api"] > div[class*="language-"] { 289 | margin-top: -8px; 290 | } 291 | 292 | .vp-doc [class*="language-"] pre, 293 | .vp-doc [class*="language-"] code { 294 | /*rtl:ignore*/ 295 | direction: ltr; 296 | /*rtl:ignore*/ 297 | text-align: left; 298 | white-space: pre; 299 | word-spacing: normal; 300 | word-break: normal; 301 | word-wrap: normal; 302 | -moz-tab-size: 4; 303 | -o-tab-size: 4; 304 | tab-size: 4; 305 | -webkit-hyphens: none; 306 | -moz-hyphens: none; 307 | -ms-hyphens: none; 308 | hyphens: none; 309 | } 310 | 311 | .vp-doc [class*="language-"] pre { 312 | position: relative; 313 | z-index: 1; 314 | margin: 0; 315 | padding: 20px 0; 316 | background: transparent; 317 | overflow-x: auto; 318 | } 319 | 320 | .vp-doc [class*="language-"] code { 321 | display: block; 322 | padding: 0 24px; 323 | width: fit-content; 324 | min-width: 100%; 325 | line-height: var(--vp-code-line-height); 326 | font-size: var(--vp-code-font-size); 327 | color: var(--vp-code-block-color); 328 | transition: color 0.5s; 329 | } 330 | 331 | .vp-doc [class*="language-"] code .highlighted { 332 | background-color: var(--vp-code-line-highlight-color); 333 | transition: background-color 0.5s; 334 | margin: 0 -24px; 335 | padding: 0 24px; 336 | width: calc(100% + 2 * 24px); 337 | display: inline-block; 338 | } 339 | 340 | .vp-doc [class*="language-"] code .highlighted.error { 341 | background-color: var(--vp-code-line-error-color); 342 | } 343 | 344 | .vp-doc [class*="language-"] code .highlighted.warning { 345 | background-color: var(--vp-code-line-warning-color); 346 | } 347 | 348 | .vp-doc [class*="language-"] code .diff { 349 | transition: background-color 0.5s; 350 | margin: 0 -24px; 351 | padding: 0 24px; 352 | width: calc(100% + 2 * 24px); 353 | display: inline-block; 354 | } 355 | 356 | .vp-doc [class*="language-"] code .diff::before { 357 | position: absolute; 358 | left: 10px; 359 | } 360 | 361 | .vp-doc [class*="language-"] .has-focused-lines .line:not(.has-focus) { 362 | filter: blur(0.095rem); 363 | opacity: 0.4; 364 | transition: filter 0.35s, opacity 0.35s; 365 | } 366 | 367 | .vp-doc [class*="language-"] .has-focused-lines .line:not(.has-focus) { 368 | opacity: 0.7; 369 | transition: filter 0.35s, opacity 0.35s; 370 | } 371 | 372 | .vp-doc [class*="language-"]:hover .has-focused-lines .line:not(.has-focus) { 373 | filter: blur(0); 374 | opacity: 1; 375 | } 376 | 377 | .vp-doc [class*="language-"] code .diff.remove { 378 | background-color: var(--vp-code-line-diff-remove-color); 379 | opacity: 0.7; 380 | } 381 | 382 | .vp-doc [class*="language-"] code .diff.remove::before { 383 | content: "-"; 384 | color: var(--vp-code-line-diff-remove-symbol-color); 385 | } 386 | 387 | .vp-doc [class*="language-"] code .diff.add { 388 | background-color: var(--vp-code-line-diff-add-color); 389 | } 390 | 391 | .vp-doc [class*="language-"] code .diff.add::before { 392 | content: "+"; 393 | color: var(--vp-code-line-diff-add-symbol-color); 394 | } 395 | 396 | .vp-doc div[class*="language-"].line-numbers-mode { 397 | /*rtl:ignore*/ 398 | padding-left: 32px; 399 | } 400 | 401 | .vp-doc .line-numbers-wrapper { 402 | position: absolute; 403 | top: 0; 404 | bottom: 0; 405 | /*rtl:ignore*/ 406 | left: 0; 407 | z-index: 3; 408 | /*rtl:ignore*/ 409 | border-right: 1px solid var(--vp-code-block-divider-color); 410 | padding-top: 20px; 411 | width: 32px; 412 | text-align: center; 413 | font-family: var(--vp-font-family-mono); 414 | line-height: var(--vp-code-line-height); 415 | font-size: var(--vp-code-font-size); 416 | color: var(--vp-code-line-number-color); 417 | transition: border-color 0.5s, color 0.5s; 418 | } 419 | 420 | .vp-doc [class*="language-"] > button.copy { 421 | /*rtl:ignore*/ 422 | direction: ltr; 423 | position: absolute; 424 | top: 12px; 425 | /*rtl:ignore*/ 426 | right: 12px; 427 | z-index: 3; 428 | border: 1px solid var(--vp-code-copy-code-border-color); 429 | border-radius: 4px; 430 | width: 40px; 431 | height: 40px; 432 | background-color: var(--vp-code-copy-code-bg); 433 | opacity: 0; 434 | cursor: pointer; 435 | background-image: var(--vp-icon-copy); 436 | background-position: 50%; 437 | background-size: 20px; 438 | background-repeat: no-repeat; 439 | transition: border-color 0.25s, background-color 0.25s, opacity 0.25s; 440 | } 441 | 442 | .vp-doc [class*="language-"]:hover > button.copy, 443 | .vp-doc [class*="language-"] > button.copy:focus { 444 | opacity: 1; 445 | } 446 | 447 | .vp-doc [class*="language-"] > button.copy:hover, 448 | .vp-doc [class*="language-"] > button.copy.copied { 449 | border-color: var(--vp-code-copy-code-hover-border-color); 450 | background-color: var(--vp-code-copy-code-hover-bg); 451 | } 452 | 453 | .vp-doc [class*="language-"] > button.copy.copied, 454 | .vp-doc [class*="language-"] > button.copy:hover.copied { 455 | /*rtl:ignore*/ 456 | border-radius: 0 4px 4px 0; 457 | background-color: var(--vp-code-copy-code-hover-bg); 458 | background-image: var(--vp-icon-copied); 459 | } 460 | 461 | .vp-doc [class*="language-"] > button.copy.copied::before, 462 | .vp-doc [class*="language-"] > button.copy:hover.copied::before { 463 | position: relative; 464 | top: -1px; 465 | /*rtl:ignore*/ 466 | transform: translateX(calc(-100% - 1px)); 467 | display: flex; 468 | justify-content: center; 469 | align-items: center; 470 | border: 1px solid var(--vp-code-copy-code-hover-border-color); 471 | /*rtl:ignore*/ 472 | border-right: 0; 473 | border-radius: 4px 0 0 4px; 474 | padding: 0 10px; 475 | width: fit-content; 476 | height: 40px; 477 | text-align: center; 478 | font-size: 12px; 479 | font-weight: 500; 480 | color: var(--vp-code-copy-code-active-text); 481 | background-color: var(--vp-code-copy-code-hover-bg); 482 | white-space: nowrap; 483 | content: var(--vp-code-copy-copied-text-content); 484 | } 485 | 486 | .vp-doc [class*="language-"] > span.lang { 487 | position: absolute; 488 | top: 2px; 489 | /*rtl:ignore*/ 490 | right: 8px; 491 | z-index: 2; 492 | font-size: 12px; 493 | font-weight: 500; 494 | color: var(--vp-code-lang-color); 495 | transition: color 0.4s, opacity 0.4s; 496 | } 497 | 498 | .vp-doc [class*="language-"]:hover > button.copy + span.lang, 499 | .vp-doc [class*="language-"] > button.copy:focus + span.lang { 500 | opacity: 0; 501 | } 502 | 503 | /** 504 | * Component: Team 505 | * -------------------------------------------------------------------------- */ 506 | 507 | .vp-doc .VPTeamMembers { 508 | margin-top: 24px; 509 | } 510 | 511 | .vp-doc .VPTeamMembers.small.count-1 .container { 512 | margin: 0 !important; 513 | max-width: calc((100% - 24px) / 2) !important; 514 | } 515 | 516 | .vp-doc .VPTeamMembers.small.count-2 .container, 517 | .vp-doc .VPTeamMembers.small.count-3 .container { 518 | max-width: 100% !important; 519 | } 520 | 521 | .vp-doc .VPTeamMembers.medium.count-1 .container { 522 | margin: 0 !important; 523 | max-width: calc((100% - 24px) / 2) !important; 524 | } 525 | 526 | /** 527 | * External links 528 | * -------------------------------------------------------------------------- */ 529 | 530 | /* prettier-ignore */ 531 | :is(.vp-external-link-icon, .vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(.no-icon)::after { 532 | display: inline-block; 533 | margin-top: -1px; 534 | margin-left: 4px; 535 | width: 11px; 536 | height: 11px; 537 | background: currentColor; 538 | color: var(--vp-c-text-3); 539 | flex-shrink: 0; 540 | --icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E"); 541 | -webkit-mask-image: var(--icon); 542 | mask-image: var(--icon); 543 | } 544 | 545 | .vp-external-link-icon::after { 546 | content: ""; 547 | } 548 | 549 | /* prettier-ignore */ 550 | .external-link-icon-enabled :is(.vp-doc a[href*='://'], .vp-doc a[target='_blank'])::after { 551 | content: ''; 552 | color: currentColor; 553 | } 554 | 555 | :global(.vp-adaptive-theme) { 556 | max-width: 98vw !important; 557 | } 558 | -------------------------------------------------------------------------------- /src/style/doc.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Headings 3 | * -------------------------------------------------------------------------- */ 4 | 5 | .vp-doc h1, 6 | .vp-doc h2, 7 | .vp-doc h3, 8 | .vp-doc h4, 9 | .vp-doc h5, 10 | .vp-doc h6 { 11 | position: relative; 12 | font-weight: 600; 13 | outline: none; 14 | } 15 | 16 | .vp-doc h1 { 17 | letter-spacing: -0.02em; 18 | line-height: 40px; 19 | font-size: 28px; 20 | } 21 | 22 | .vp-doc h2 { 23 | margin: 48px 0 16px; 24 | border-top: 1px solid var(--vp-c-divider); 25 | padding-top: 24px; 26 | letter-spacing: -0.02em; 27 | line-height: 32px; 28 | font-size: 24px; 29 | } 30 | 31 | .vp-doc h3 { 32 | margin: 32px 0 0; 33 | letter-spacing: -0.01em; 34 | line-height: 28px; 35 | font-size: 20px; 36 | } 37 | 38 | .vp-doc .header-anchor { 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | margin-left: -0.87em; 43 | font-weight: 500; 44 | user-select: none; 45 | opacity: 0; 46 | text-decoration: none; 47 | transition: 48 | color 0.25s, 49 | opacity 0.25s; 50 | } 51 | 52 | .vp-doc .header-anchor:before { 53 | content: var(--vp-header-anchor-symbol); 54 | } 55 | 56 | .vp-doc h1:hover .header-anchor, 57 | .vp-doc h1 .header-anchor:focus, 58 | .vp-doc h2:hover .header-anchor, 59 | .vp-doc h2 .header-anchor:focus, 60 | .vp-doc h3:hover .header-anchor, 61 | .vp-doc h3 .header-anchor:focus, 62 | .vp-doc h4:hover .header-anchor, 63 | .vp-doc h4 .header-anchor:focus, 64 | .vp-doc h5:hover .header-anchor, 65 | .vp-doc h5 .header-anchor:focus, 66 | .vp-doc h6:hover .header-anchor, 67 | .vp-doc h6 .header-anchor:focus { 68 | opacity: 1; 69 | } 70 | 71 | @media (min-width: 768px) { 72 | .vp-doc h1 { 73 | letter-spacing: -0.02em; 74 | line-height: 40px; 75 | font-size: 32px; 76 | } 77 | } 78 | 79 | .vp-doc h2 .header-anchor { 80 | top: 24px; 81 | } 82 | 83 | /** 84 | * Paragraph and inline elements 85 | * -------------------------------------------------------------------------- */ 86 | 87 | .vp-doc p, 88 | .vp-doc summary { 89 | margin: 16px 0; 90 | } 91 | 92 | .vp-doc p { 93 | line-height: 28px; 94 | } 95 | 96 | .vp-doc blockquote { 97 | margin: 16px 0; 98 | border-left: 2px solid var(--vp-c-divider); 99 | padding-left: 16px; 100 | transition: border-color 0.5s; 101 | } 102 | 103 | .vp-doc blockquote > p { 104 | margin: 0; 105 | font-size: 16px; 106 | color: var(--vp-c-text-2); 107 | transition: color 0.5s; 108 | } 109 | 110 | .vp-doc a { 111 | font-weight: 500; 112 | color: var(--vp-c-brand-1); 113 | text-decoration: underline; 114 | text-underline-offset: 2px; 115 | transition: 116 | color 0.25s, 117 | opacity 0.25s; 118 | } 119 | 120 | .vp-doc a:hover { 121 | color: var(--vp-c-brand-2); 122 | } 123 | 124 | .vp-doc strong { 125 | font-weight: 600; 126 | } 127 | 128 | /** 129 | * Lists 130 | * -------------------------------------------------------------------------- */ 131 | 132 | .vp-doc ul, 133 | .vp-doc ol { 134 | padding-left: 1.25rem; 135 | margin: 16px 0; 136 | } 137 | 138 | .vp-doc ul { 139 | list-style: disc; 140 | } 141 | 142 | .vp-doc ol { 143 | list-style: decimal; 144 | } 145 | 146 | .vp-doc li + li { 147 | margin-top: 8px; 148 | } 149 | 150 | .vp-doc li > ol, 151 | .vp-doc li > ul { 152 | margin: 8px 0 0; 153 | } 154 | 155 | /** 156 | * Table 157 | * -------------------------------------------------------------------------- */ 158 | 159 | .vp-doc-table table { 160 | display: block; 161 | border-collapse: collapse; 162 | margin: 20px 0; 163 | overflow-x: auto; 164 | } 165 | 166 | .vp-doc-table tr { 167 | background-color: var(--vp-c-bg); 168 | border-top: 1px solid var(--vp-c-divider); 169 | transition: background-color 0.5s; 170 | } 171 | 172 | .vp-doc-table tr:nth-child(2n) { 173 | background-color: var(--vp-c-bg-soft); 174 | } 175 | 176 | .vp-doc-table th, 177 | .vp-doc-table td { 178 | border: 1px solid var(--vp-c-divider); 179 | padding: 8px 16px; 180 | } 181 | 182 | .vp-doc-table th { 183 | text-align: left; 184 | font-size: 14px; 185 | font-weight: 600; 186 | color: var(--vp-c-text-2); 187 | background-color: var(--vp-c-bg-soft); 188 | } 189 | 190 | .vp-doc-table td { 191 | font-size: 14px; 192 | } 193 | 194 | /** 195 | * Decorational elements 196 | * -------------------------------------------------------------------------- */ 197 | 198 | .vp-doc hr { 199 | margin: 16px 0; 200 | border: none; 201 | border-top: 1px solid var(--vp-c-divider); 202 | } 203 | 204 | /** 205 | * Custom Block 206 | * -------------------------------------------------------------------------- */ 207 | 208 | .vp-doc .custom-block { 209 | margin: 16px 0; 210 | } 211 | 212 | .vp-doc .custom-block p { 213 | margin: 8px 0; 214 | line-height: 24px; 215 | } 216 | 217 | .vp-doc .custom-block p:first-child { 218 | margin: 0; 219 | } 220 | 221 | .vp-doc .custom-block div[class*='language-'] { 222 | margin: 8px 0; 223 | border-radius: 8px; 224 | } 225 | 226 | .vp-doc .custom-block div[class*='language-'] code { 227 | font-weight: 400; 228 | background-color: transparent; 229 | } 230 | 231 | .vp-doc .custom-block .vp-code-group .tabs { 232 | margin: 0; 233 | border-radius: 8px 8px 0 0; 234 | } 235 | 236 | /** 237 | * Code 238 | * -------------------------------------------------------------------------- */ 239 | 240 | /* inline code */ 241 | .vp-doc :not(pre, h1, h2, h3, h4, h5, h6) > code { 242 | font-size: var(--vp-code-font-size); 243 | color: var(--vp-code-color); 244 | } 245 | 246 | .vp-doc :not(pre) > code { 247 | border-radius: 4px; 248 | padding: 3px 6px; 249 | background-color: var(--vp-code-bg); 250 | transition: 251 | color 0.25s, 252 | background-color 0.5s; 253 | } 254 | 255 | .vp-doc a > code { 256 | color: var(--vp-code-link-color); 257 | } 258 | 259 | .vp-doc a:hover > code { 260 | color: var(--vp-code-link-hover-color); 261 | } 262 | 263 | .vp-doc h1 > code, 264 | .vp-doc h2 > code, 265 | .vp-doc h3 > code { 266 | font-size: 0.9em; 267 | } 268 | 269 | .vp-doc div[class*='language-'], 270 | .vp-block { 271 | position: relative; 272 | margin: 16px -24px; 273 | max-width: 100vw; 274 | background-color: var(--vp-code-block-bg); 275 | overflow-x: auto; 276 | transition: background-color 0.5s; 277 | } 278 | 279 | @media (min-width: 640px) { 280 | .vp-doc div[class*='language-'], 281 | .vp-block { 282 | border-radius: 8px; 283 | margin: 16px 0; 284 | } 285 | } 286 | 287 | @media (max-width: 639px) { 288 | .vp-doc li div[class*='language-'] { 289 | border-radius: 8px 0 0 8px; 290 | } 291 | } 292 | 293 | .vp-doc div[class*='language-'] + div[class*='language-'], 294 | .vp-doc div[class$='-api'] + div[class*='language-'], 295 | .vp-doc div[class*='language-'] + div[class$='-api'] > div[class*='language-'] { 296 | margin-top: -8px; 297 | } 298 | 299 | .vp-doc [class*='language-'] pre, 300 | .vp-doc [class*='language-'] code { 301 | /*rtl:ignore*/ 302 | direction: ltr; 303 | /*rtl:ignore*/ 304 | text-align: left; 305 | white-space: pre; 306 | word-spacing: normal; 307 | word-break: normal; 308 | word-wrap: normal; 309 | -moz-tab-size: 4; 310 | -o-tab-size: 4; 311 | tab-size: 4; 312 | -webkit-hyphens: none; 313 | -moz-hyphens: none; 314 | -ms-hyphens: none; 315 | hyphens: none; 316 | } 317 | 318 | .vp-doc [class*='language-'] pre { 319 | position: relative; 320 | z-index: 1; 321 | margin: 0; 322 | padding: 20px 0; 323 | background: transparent; 324 | overflow-x: auto; 325 | } 326 | 327 | .vp-doc [class*='language-'] code { 328 | display: block; 329 | padding: 0 24px; 330 | width: fit-content; 331 | min-width: 100%; 332 | line-height: var(--vp-code-line-height); 333 | font-size: var(--vp-code-font-size); 334 | color: var(--vp-code-block-color); 335 | transition: color 0.5s; 336 | } 337 | 338 | .vp-doc [class*='language-'] code .highlighted { 339 | background-color: var(--vp-code-line-highlight-color); 340 | transition: background-color 0.5s; 341 | margin: 0 -24px; 342 | padding: 0 24px; 343 | width: calc(100% + 2 * 24px); 344 | display: inline-block; 345 | } 346 | 347 | .vp-doc [class*='language-'] code .highlighted.error { 348 | background-color: var(--vp-code-line-error-color); 349 | } 350 | 351 | .vp-doc [class*='language-'] code .highlighted.warning { 352 | background-color: var(--vp-code-line-warning-color); 353 | } 354 | 355 | .vp-doc [class*='language-'] code .diff { 356 | transition: background-color 0.5s; 357 | margin: 0 -24px; 358 | padding: 0 24px; 359 | width: calc(100% + 2 * 24px); 360 | display: inline-block; 361 | } 362 | 363 | .vp-doc [class*='language-'] code .diff::before { 364 | position: absolute; 365 | left: 10px; 366 | } 367 | 368 | .vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) { 369 | filter: blur(0.095rem); 370 | opacity: 0.4; 371 | transition: 372 | filter 0.35s, 373 | opacity 0.35s; 374 | } 375 | 376 | .vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) { 377 | opacity: 0.7; 378 | transition: 379 | filter 0.35s, 380 | opacity 0.35s; 381 | } 382 | 383 | .vp-doc [class*='language-']:hover .has-focused-lines .line:not(.has-focus) { 384 | filter: blur(0); 385 | opacity: 1; 386 | } 387 | 388 | .vp-doc [class*='language-'] code .diff.remove { 389 | background-color: var(--vp-code-line-diff-remove-color); 390 | opacity: 0.7; 391 | } 392 | 393 | .vp-doc [class*='language-'] code .diff.remove::before { 394 | content: '-'; 395 | color: var(--vp-code-line-diff-remove-symbol-color); 396 | } 397 | 398 | .vp-doc [class*='language-'] code .diff.add { 399 | background-color: var(--vp-code-line-diff-add-color); 400 | } 401 | 402 | .vp-doc [class*='language-'] code .diff.add::before { 403 | content: '+'; 404 | color: var(--vp-code-line-diff-add-symbol-color); 405 | } 406 | 407 | .vp-doc div[class*='language-'].line-numbers-mode { 408 | /*rtl:ignore*/ 409 | padding-left: 32px; 410 | } 411 | 412 | .vp-doc .line-numbers-wrapper { 413 | position: absolute; 414 | top: 0; 415 | bottom: 0; 416 | /*rtl:ignore*/ 417 | left: 0; 418 | z-index: 3; 419 | /*rtl:ignore*/ 420 | border-right: 1px solid var(--vp-code-block-divider-color); 421 | padding-top: 20px; 422 | width: 32px; 423 | text-align: center; 424 | font-family: var(--vp-font-family-mono); 425 | line-height: var(--vp-code-line-height); 426 | font-size: var(--vp-code-font-size); 427 | color: var(--vp-code-line-number-color); 428 | transition: 429 | border-color 0.5s, 430 | color 0.5s; 431 | } 432 | 433 | .vp-doc [class*='language-'] > button.copy { 434 | /*rtl:ignore*/ 435 | direction: ltr; 436 | position: absolute; 437 | top: 12px; 438 | /*rtl:ignore*/ 439 | right: 12px; 440 | z-index: 3; 441 | border: 1px solid var(--vp-code-copy-code-border-color); 442 | border-radius: 4px; 443 | width: 40px; 444 | height: 40px; 445 | background-color: var(--vp-code-copy-code-bg); 446 | opacity: 0; 447 | cursor: pointer; 448 | background-image: var(--vp-icon-copy); 449 | background-position: 50%; 450 | background-size: 20px; 451 | background-repeat: no-repeat; 452 | transition: 453 | border-color 0.25s, 454 | background-color 0.25s, 455 | opacity 0.25s; 456 | } 457 | 458 | .vp-doc [class*='language-']:hover > button.copy, 459 | .vp-doc [class*='language-'] > button.copy:focus { 460 | opacity: 1; 461 | } 462 | 463 | .vp-doc [class*='language-'] > button.copy:hover, 464 | .vp-doc [class*='language-'] > button.copy.copied { 465 | border-color: var(--vp-code-copy-code-hover-border-color); 466 | background-color: var(--vp-code-copy-code-hover-bg); 467 | } 468 | 469 | .vp-doc [class*='language-'] > button.copy.copied, 470 | .vp-doc [class*='language-'] > button.copy:hover.copied { 471 | /*rtl:ignore*/ 472 | border-radius: 0 4px 4px 0; 473 | background-color: var(--vp-code-copy-code-hover-bg); 474 | background-image: var(--vp-icon-copied); 475 | } 476 | 477 | .vp-doc [class*='language-'] > button.copy.copied::before, 478 | .vp-doc [class*='language-'] > button.copy:hover.copied::before { 479 | position: relative; 480 | top: -1px; 481 | /*rtl:ignore*/ 482 | transform: translateX(calc(-100% - 1px)); 483 | display: flex; 484 | justify-content: center; 485 | align-items: center; 486 | border: 1px solid var(--vp-code-copy-code-hover-border-color); 487 | /*rtl:ignore*/ 488 | border-right: 0; 489 | border-radius: 4px 0 0 4px; 490 | padding: 0 10px; 491 | width: fit-content; 492 | height: 40px; 493 | text-align: center; 494 | font-size: 12px; 495 | font-weight: 500; 496 | color: var(--vp-code-copy-code-active-text); 497 | background-color: var(--vp-code-copy-code-hover-bg); 498 | white-space: nowrap; 499 | content: var(--vp-code-copy-copied-text-content); 500 | } 501 | 502 | .vp-doc [class*='language-'] > span.lang { 503 | position: absolute; 504 | top: 2px; 505 | /*rtl:ignore*/ 506 | right: 8px; 507 | z-index: 2; 508 | font-size: 12px; 509 | font-weight: 500; 510 | color: var(--vp-code-lang-color); 511 | transition: 512 | color 0.4s, 513 | opacity 0.4s; 514 | } 515 | 516 | .vp-doc [class*='language-']:hover > button.copy + span.lang, 517 | .vp-doc [class*='language-'] > button.copy:focus + span.lang { 518 | opacity: 0; 519 | } 520 | 521 | /** 522 | * Component: Team 523 | * -------------------------------------------------------------------------- */ 524 | 525 | .vp-doc .VPTeamMembers { 526 | margin-top: 24px; 527 | } 528 | 529 | .vp-doc .VPTeamMembers.small.count-1 .container { 530 | margin: 0 !important; 531 | max-width: calc((100% - 24px) / 2) !important; 532 | } 533 | 534 | .vp-doc .VPTeamMembers.small.count-2 .container, 535 | .vp-doc .VPTeamMembers.small.count-3 .container { 536 | max-width: 100% !important; 537 | } 538 | 539 | .vp-doc .VPTeamMembers.medium.count-1 .container { 540 | margin: 0 !important; 541 | max-width: calc((100% - 24px) / 2) !important; 542 | } 543 | 544 | /** 545 | * External links 546 | * -------------------------------------------------------------------------- */ 547 | 548 | /* prettier-ignore */ 549 | :is(.vp-external-link-icon, .vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(.no-icon)::after { 550 | display: inline-block; 551 | margin-top: -1px; 552 | margin-left: 4px; 553 | width: 11px; 554 | height: 11px; 555 | background: currentColor; 556 | color: var(--vp-c-text-3); 557 | flex-shrink: 0; 558 | --icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E"); 559 | -webkit-mask-image: var(--icon); 560 | mask-image: var(--icon); 561 | } 562 | 563 | .vp-external-link-icon::after { 564 | content: ''; 565 | } 566 | 567 | /* prettier-ignore */ 568 | .external-link-icon-enabled :is(.vp-doc a[href*='://'], .vp-doc a[target='_blank'])::after { 569 | content: ''; 570 | color: currentColor; 571 | } 572 | -------------------------------------------------------------------------------- /src/style/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Customize default theme styling by overriding CSS variables: 3 | * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css 4 | */ 5 | 6 | /** 7 | * Colors 8 | * 9 | * Each colors have exact same color scale system with 3 levels of solid 10 | * colors with different brightness, and 1 soft color. 11 | * 12 | * - `XXX-1`: The most solid color used mainly for colored text. It must 13 | * satisfy the contrast ratio against when used on top of `XXX-soft`. 14 | * 15 | * - `XXX-2`: The color used mainly for hover state of the button. 16 | * 17 | * - `XXX-3`: The color for solid background, such as bg color of the button. 18 | * It must satisfy the contrast ratio with pure white (#ffffff) text on 19 | * top of it. 20 | * 21 | * - `XXX-soft`: The color used for subtle background such as custom container 22 | * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors 23 | * on top of it. 24 | * 25 | * The soft color must be semi transparent alpha channel. This is crucial 26 | * because it allows adding multiple "soft" colors on top of each other 27 | * to create a accent, such as when having inline code block inside 28 | * custom containers. 29 | * 30 | * - `default`: The color used purely for subtle indication without any 31 | * special meanings attched to it such as bg color for menu hover state. 32 | * 33 | * - `brand`: Used for primary brand colors, such as link text, button with 34 | * brand theme, etc. 35 | * 36 | * - `tip`: Used to indicate useful information. The default theme uses the 37 | * brand color for this by default. 38 | * 39 | * - `warning`: Used to indicate warning to the users. Used in custom 40 | * container, badges, etc. 41 | * 42 | * - `danger`: Used to show error, or dangerous message to the users. Used 43 | * in custom container, badges, etc. 44 | * -------------------------------------------------------------------------- */ 45 | 46 | :root { 47 | /* --vp-c-brand-1: #ca4b22; */ 48 | --vp-c-brand-1: #286090; 49 | --vp-c-brand-2: #bc6063; 50 | --vp-c-brand-3: #b34e52; 51 | --vp-c-brand-soft: #83d0da50; 52 | --vp-c-bg-alt: #f9f9f9; 53 | 54 | --vp-c-yellow-1: #edb253; 55 | --vp-c-yellow-2: #daac61; 56 | --vp-c-yellow-3: #e6cc78; 57 | 58 | --vp-c-red-1: #b34e52; 59 | --vp-c-red-2: #bc6063; 60 | --vp-c-red-3: #cb7676; 61 | } 62 | 63 | .dark { 64 | --sk-theme-1-hsl: hsl(38.77deg 71.8% 58.64%); 65 | /* --vp-c-brand-1: #cb7676; */ 66 | --vp-c-brand-1: var(--sk-theme-1-hsl); 67 | --vp-c-brand-2: #bc6063; 68 | --vp-c-brand-3: #b34e52; 69 | --vp-c-brand-soft: #83d0da50; 70 | --vp-c-bg-alt: #18181b; 71 | 72 | --vp-c-yellow-1: #e6cc78; 73 | --vp-c-yellow-2: #daac61; 74 | --vp-c-yellow-3: #edb253; 75 | 76 | --vp-c-red-1: #cb7676; 77 | --vp-c-red-2: #bc6063; 78 | --vp-c-red-3: #b34e52; 79 | } 80 | 81 | :root { 82 | --vp-c-default-1: var(--vp-c-gray-1); 83 | --vp-c-default-2: var(--vp-c-gray-2); 84 | --vp-c-default-3: var(--vp-c-gray-3); 85 | --vp-c-default-soft: var(--vp-c-gray-soft); 86 | 87 | --vp-c-tip-1: var(--vp-c-brand-1); 88 | --vp-c-tip-2: var(--vp-c-brand-2); 89 | --vp-c-tip-3: var(--vp-c-brand-3); 90 | --vp-c-tip-soft: var(--vp-c-brand-soft); 91 | 92 | --vp-c-warning-1: var(--vp-c-yellow-1); 93 | --vp-c-warning-2: var(--vp-c-yellow-2); 94 | --vp-c-warning-3: var(--vp-c-yellow-3); 95 | --vp-c-warning-soft: var(--vp-c-yellow-soft); 96 | 97 | --vp-c-danger-1: var(--vp-c-red-1); 98 | --vp-c-danger-2: var(--vp-c-red-2); 99 | --vp-c-danger-3: var(--vp-c-red-3); 100 | --vp-c-danger-soft: var(--vp-c-red-soft); 101 | } 102 | 103 | :root { 104 | -vp-c-text-1: rgba(42, 40, 47); 105 | -vp-c-text-2: rgba(42, 40, 47, 0.78); 106 | -vp-c-text-3: rgba(42, 40, 47, 0.56); 107 | --black-text-1: rgba(42, 40, 47); 108 | } 109 | 110 | .dark { 111 | --vp-c-text-1: rgba(255, 255, 245, 0.86); 112 | --vp-c-text-2: rgba(235, 235, 245, 0.6); 113 | --vp-c-text-3: rgba(235, 235, 245, 0.38); 114 | } 115 | 116 | /** 117 | * Component: Button 118 | * -------------------------------------------------------------------------- */ 119 | 120 | :root { 121 | --vp-button-brand-border: transparent; 122 | --vp-button-brand-text: var(--vp-c-white); 123 | --vp-button-brand-bg: var(--vp-c-brand-1); 124 | --vp-button-brand-hover-border: transparent; 125 | --vp-button-brand-hover-text: var(--vp-c-white); 126 | --vp-button-brand-hover-bg: var(--vp-c-brand-2); 127 | --vp-button-brand-active-border: transparent; 128 | --vp-button-brand-active-text: var(--vp-c-white); 129 | --vp-button-brand-active-bg: var(--vp-c-brand-1); 130 | } 131 | 132 | .dark { 133 | --vp-button-brand-text: var(--black-text-1); 134 | --vp-button-brand-bg: var(--vp-c-brand-2); 135 | --vp-button-brand-hover-text: var(--black-text-1); 136 | --vp-button-brand-hover-bg: var(--vp-c-brand-1); 137 | --vp-button-brand-active-text: var(--black-text-1); 138 | --vp-button-brand-active-bg: var(--vp-c-brand-3); 139 | } 140 | 141 | /** 142 | * Component: Home 143 | * -------------------------------------------------------------------------- */ 144 | 145 | :root { 146 | --vp-home-hero-name-color: var(--vp-c-brand-1); 147 | } 148 | 149 | @media (min-width: 640px) { 150 | :root { 151 | --vp-home-hero-image-filter: blur(56px); 152 | } 153 | } 154 | 155 | @media (min-width: 960px) { 156 | :root { 157 | --vp-home-hero-image-filter: blur(72px); 158 | } 159 | } 160 | 161 | /** 162 | * Component: Custom Block 163 | * -------------------------------------------------------------------------- */ 164 | 165 | :root { 166 | --vp-custom-block-tip-border: transparent; 167 | --vp-custom-block-tip-text: var(--vp-c-text-1); 168 | --vp-custom-block-tip-bg: var(--vp-c-brand-soft); 169 | --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); 170 | } 171 | 172 | /** 173 | * Component: Algolia 174 | * -------------------------------------------------------------------------- */ 175 | 176 | .DocSearch { 177 | --docsearch-primary-color: var(--vp-c-brand-1) !important; 178 | } 179 | 180 | .dark { 181 | --sdt-bg-main: #585858; 182 | --sdt-shadow-color: #333; 183 | --sdt-color: #eee; 184 | --sdt-clock-color: var(--sdt-color); 185 | --sdt-clock-color-hover: var(--sdt-color); 186 | --sdt-clock-time-bg: transparent; 187 | --sdt-clock-time-bg-hover: transparent; 188 | --sdt-clock-disabled: #b22222; 189 | --sdt-clock-disabled-bg: var(--sdt-bg-main); 190 | --sdt-clock-selected-bg: var(--sdt-bg-selected); 191 | --sdt-header-color: #eee; 192 | --sdt-bg-selected: #e1ac4a; 193 | --sdt-table-disabled-date: #b22222; 194 | --sdt-table-disabled-date-bg: var(--sdt-bg-main); 195 | --sdt-table-data-bg-hover: #777; 196 | --sdt-table-selected-bg: var(--sdt-bg-selected); 197 | --sdt-header-btn-bg-hover: #777; 198 | --sdt-color-selected: #fff; 199 | --sdt-table-today-indicator: #ccc; 200 | 201 | --sdt-clock-bg: #999; 202 | 203 | /* custom buttons */ 204 | --sdt-today-bg: #e4a124; 205 | --sdt-today-color: #fff; 206 | --sdt-clear-color: #666; 207 | --sdt-clear-bg: #ddd; 208 | --sdt-clear-hover-color: #fff; 209 | --sdt-clear-hover-bg: #dc3545; 210 | } 211 | .dark button { 212 | border-radius: 0; 213 | } 214 | 215 | h1 { 216 | margin-top: 16px; 217 | margin-bottom: 28px; 218 | } 219 | body { 220 | overflow: scroll; 221 | } 222 | 223 | .vp-doc { 224 | h1 > a { 225 | color: inherit; 226 | text-decoration: none; 227 | } 228 | h2 > a, 229 | h3 > a, 230 | h4 > a, 231 | h5 > a, 232 | h6 > a { 233 | text-decoration: none; 234 | color: inherit; 235 | } 236 | } 237 | .toc { 238 | .toc-level-1 { 239 | font-size: $font-size-3; 240 | font-weight: $font-weight-bold; 241 | } 242 | } 243 | 244 | .dark .light-toggle { 245 | display: none; 246 | } 247 | html:not(.dark) .dark-toggle { 248 | display: none; 249 | } 250 | .nav-footer { 251 | display: flex; 252 | justify-content: space-between; 253 | padding-left: 10px; 254 | } 255 | 256 | .sdt-component-wrap input { 257 | border: 1px solid var(--vp-c-text-1); 258 | padding: 4px; 259 | border-radius: 4px; 260 | } 261 | -------------------------------------------------------------------------------- /src/style/vars.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Colors: Solid 3 | * -------------------------------------------------------------------------- */ 4 | 5 | :root { 6 | --vp-c-white: #ffffff; 7 | --vp-c-black: #000000; 8 | 9 | --vp-c-neutral: var(--vp-c-black); 10 | --vp-c-neutral-inverse: var(--vp-c-white); 11 | } 12 | 13 | .dark { 14 | --vp-c-neutral: var(--vp-c-white); 15 | --vp-c-neutral-inverse: var(--vp-c-black); 16 | } 17 | 18 | /** 19 | * Colors: Palette 20 | * 21 | * The primitive colors used for accent colors. These colors are referenced 22 | * by functional colors such as "Text", "Background", or "Brand". 23 | * 24 | * Each colors have exact same color scale system with 3 levels of solid 25 | * colors with different brightness, and 1 soft color. 26 | * 27 | * - `XXX-1`: The most solid color used mainly for colored text. It must 28 | * satisfy the contrast ratio against when used on top of `XXX-soft`. 29 | * 30 | * - `XXX-2`: The color used mainly for hover state of the button. 31 | * 32 | * - `XXX-3`: The color for solid background, such as bg color of the button. 33 | * It must satisfy the contrast ratio with pure white (#ffffff) text on 34 | * top of it. 35 | * 36 | * - `XXX-soft`: The color used for subtle background such as custom container 37 | * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors 38 | * on top of it. 39 | * 40 | * The soft color must be semi transparent alpha channel. This is crucial 41 | * because it allows adding multiple "soft" colors on top of each other 42 | * to create a accent, such as when having inline code block inside 43 | * custom containers. 44 | * -------------------------------------------------------------------------- */ 45 | 46 | :root { 47 | --vp-c-gray-1: #dddde3; 48 | --vp-c-gray-2: #e4e4e9; 49 | --vp-c-gray-3: #ebebef; 50 | --vp-c-gray-soft: rgba(142, 150, 170, 0.14); 51 | 52 | --vp-c-indigo-1: #3451b2; 53 | --vp-c-indigo-2: #3a5ccc; 54 | --vp-c-indigo-3: #5672cd; 55 | --vp-c-indigo-soft: rgba(100, 108, 255, 0.14); 56 | 57 | --vp-c-purple-1: #6f42c1; 58 | --vp-c-purple-2: #7e4cc9; 59 | --vp-c-purple-3: #8e5cd9; 60 | --vp-c-purple-soft: rgba(159, 122, 234, 0.14); 61 | 62 | --vp-c-green-1: #18794e; 63 | --vp-c-green-2: #299764; 64 | --vp-c-green-3: #30a46c; 65 | --vp-c-green-soft: rgba(16, 185, 129, 0.14); 66 | 67 | --vp-c-yellow-1: #915930; 68 | --vp-c-yellow-2: #946300; 69 | --vp-c-yellow-3: #9f6a00; 70 | --vp-c-yellow-soft: rgba(234, 179, 8, 0.14); 71 | 72 | --vp-c-red-1: #b8272c; 73 | --vp-c-red-2: #d5393e; 74 | --vp-c-red-3: #e0575b; 75 | --vp-c-red-soft: rgba(244, 63, 94, 0.14); 76 | 77 | --vp-c-sponsor: #db2777; 78 | } 79 | 80 | .dark { 81 | --vp-c-gray-1: #515c67; 82 | --vp-c-gray-2: #414853; 83 | --vp-c-gray-3: #32363f; 84 | --vp-c-gray-soft: rgba(101, 117, 133, 0.16); 85 | 86 | --vp-c-indigo-1: #a8b1ff; 87 | --vp-c-indigo-2: #5c73e7; 88 | --vp-c-indigo-3: #3e63dd; 89 | --vp-c-indigo-soft: rgba(100, 108, 255, 0.16); 90 | 91 | --vp-c-purple-1: #c8abfa; 92 | --vp-c-purple-2: #a879e6; 93 | --vp-c-purple-3: #8e5cd9; 94 | --vp-c-purple-soft: rgba(159, 122, 234, 0.16); 95 | 96 | --vp-c-green-1: #3dd68c; 97 | --vp-c-green-2: #30a46c; 98 | --vp-c-green-3: #298459; 99 | --vp-c-green-soft: rgba(16, 185, 129, 0.16); 100 | 101 | --vp-c-yellow-1: #f9b44e; 102 | --vp-c-yellow-2: #da8b17; 103 | --vp-c-yellow-3: #a46a0a; 104 | --vp-c-yellow-soft: rgba(234, 179, 8, 0.16); 105 | 106 | --vp-c-red-1: #f66f81; 107 | --vp-c-red-2: #f14158; 108 | --vp-c-red-3: #b62a3c; 109 | --vp-c-red-soft: rgba(244, 63, 94, 0.16); 110 | } 111 | 112 | /** 113 | * Colors: Background 114 | * 115 | * - `bg`: The bg color used for main screen. 116 | * 117 | * - `bg-alt`: The alternative bg color used in places such as "sidebar", 118 | * or "code block". 119 | * 120 | * - `bg-elv`: The elevated bg color. This is used at parts where it "floats", 121 | * such as "dialog". 122 | * 123 | * - `bg-soft`: The bg color to slightly distinguish some components from 124 | * the page. Used for things like "carbon ads" or "table". 125 | * -------------------------------------------------------------------------- */ 126 | 127 | :root { 128 | --vp-c-bg: #ffffff; 129 | --vp-c-bg-alt: #f6f6f7; 130 | --vp-c-bg-elv: #ffffff; 131 | --vp-c-bg-soft: #f6f6f7; 132 | } 133 | 134 | .dark { 135 | --vp-c-bg: #1b1b1f; 136 | --vp-c-bg-alt: #161618; 137 | --vp-c-bg-elv: #202127; 138 | --vp-c-bg-soft: #202127; 139 | } 140 | 141 | /** 142 | * Colors: Borders 143 | * 144 | * - `divider`: This is used for separators. This is used to divide sections 145 | * within the same components, such as having separator on "h2" heading. 146 | * 147 | * - `border`: This is designed for borders on interactive components. 148 | * For example this should be used for a button outline. 149 | * 150 | * - `gutter`: This is used to divide components in the page. For example 151 | * the header and the lest of the page. 152 | * -------------------------------------------------------------------------- */ 153 | 154 | :root { 155 | --vp-c-border: #c2c2c4; 156 | --vp-c-divider: #e2e2e3; 157 | --vp-c-gutter: #e2e2e3; 158 | } 159 | 160 | .dark { 161 | --vp-c-border: #3c3f44; 162 | --vp-c-divider: #2e2e32; 163 | --vp-c-gutter: #000000; 164 | } 165 | 166 | /** 167 | * Colors: Text 168 | * 169 | * - `text-1`: Used for primary text. 170 | * 171 | * - `text-2`: Used for muted texts, such as "inactive menu" or "info texts". 172 | * 173 | * - `text-3`: Used for subtle texts, such as "placeholders" or "caret icon". 174 | * -------------------------------------------------------------------------- */ 175 | 176 | :root { 177 | --vp-c-text-1: rgba(60, 60, 67); 178 | --vp-c-text-2: rgba(60, 60, 67, 0.78); 179 | --vp-c-text-3: rgba(60, 60, 67, 0.56); 180 | } 181 | 182 | .dark { 183 | --vp-c-text-1: rgba(255, 255, 245, 0.86); 184 | --vp-c-text-2: rgba(235, 235, 245, 0.6); 185 | --vp-c-text-3: rgba(235, 235, 245, 0.38); 186 | } 187 | 188 | /** 189 | * Colors: Function 190 | * 191 | * - `default`: The color used purely for subtle indication without any 192 | * special meanings attached to it such as bg color for menu hover state. 193 | * 194 | * - `brand`: Used for primary brand colors, such as link text, button with 195 | * brand theme, etc. 196 | * 197 | * - `tip`: Used to indicate useful information. The default theme uses the 198 | * brand color for this by default. 199 | * 200 | * - `warning`: Used to indicate warning to the users. Used in custom 201 | * container, badges, etc. 202 | * 203 | * - `danger`: Used to show error, or dangerous message to the users. Used 204 | * in custom container, badges, etc. 205 | * 206 | * To understand the scaling system, refer to "Colors: Palette" section. 207 | * -------------------------------------------------------------------------- */ 208 | 209 | :root { 210 | --vp-c-default-1: var(--vp-c-gray-1); 211 | --vp-c-default-2: var(--vp-c-gray-2); 212 | --vp-c-default-3: var(--vp-c-gray-3); 213 | --vp-c-default-soft: var(--vp-c-gray-soft); 214 | 215 | --vp-c-brand-1: var(--vp-c-indigo-1); 216 | --vp-c-brand-2: var(--vp-c-indigo-2); 217 | --vp-c-brand-3: var(--vp-c-indigo-3); 218 | --vp-c-brand-soft: var(--vp-c-indigo-soft); 219 | 220 | /* DEPRECATED: Use `--vp-c-brand-1` instead. */ 221 | --vp-c-brand: var(--vp-c-brand-1); 222 | 223 | --vp-c-tip-1: var(--vp-c-brand-1); 224 | --vp-c-tip-2: var(--vp-c-brand-2); 225 | --vp-c-tip-3: var(--vp-c-brand-3); 226 | --vp-c-tip-soft: var(--vp-c-brand-soft); 227 | 228 | --vp-c-note-1: var(--vp-c-brand-1); 229 | --vp-c-note-2: var(--vp-c-brand-2); 230 | --vp-c-note-3: var(--vp-c-brand-3); 231 | --vp-c-note-soft: var(--vp-c-brand-soft); 232 | 233 | --vp-c-success-1: var(--vp-c-green-1); 234 | --vp-c-success-2: var(--vp-c-green-2); 235 | --vp-c-success-3: var(--vp-c-green-3); 236 | --vp-c-success-soft: var(--vp-c-green-soft); 237 | 238 | --vp-c-important-1: var(--vp-c-purple-1); 239 | --vp-c-important-2: var(--vp-c-purple-2); 240 | --vp-c-important-3: var(--vp-c-purple-3); 241 | --vp-c-important-soft: var(--vp-c-purple-soft); 242 | 243 | --vp-c-warning-1: var(--vp-c-yellow-1); 244 | --vp-c-warning-2: var(--vp-c-yellow-2); 245 | --vp-c-warning-3: var(--vp-c-yellow-3); 246 | --vp-c-warning-soft: var(--vp-c-yellow-soft); 247 | 248 | --vp-c-danger-1: var(--vp-c-red-1); 249 | --vp-c-danger-2: var(--vp-c-red-2); 250 | --vp-c-danger-3: var(--vp-c-red-3); 251 | --vp-c-danger-soft: var(--vp-c-red-soft); 252 | 253 | --vp-c-caution-1: var(--vp-c-red-1); 254 | --vp-c-caution-2: var(--vp-c-red-2); 255 | --vp-c-caution-3: var(--vp-c-red-3); 256 | --vp-c-caution-soft: var(--vp-c-red-soft); 257 | } 258 | 259 | /** 260 | * Typography 261 | * -------------------------------------------------------------------------- */ 262 | 263 | :root { 264 | --vp-font-family-base: 'Chinese Quotes', 'Inter var', 'Inter', ui-sans-serif, 265 | system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 266 | 'Helvetica Neue', Helvetica, Arial, 'Noto Sans', sans-serif, 267 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 268 | --vp-font-family-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, 269 | Consolas, 'Liberation Mono', 'Courier New', monospace; 270 | } 271 | 272 | /** 273 | * Shadows 274 | * -------------------------------------------------------------------------- */ 275 | 276 | :root { 277 | --vp-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06); 278 | --vp-shadow-2: 0 3px 12px rgba(0, 0, 0, 0.07), 0 1px 4px rgba(0, 0, 0, 0.07); 279 | --vp-shadow-3: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08); 280 | --vp-shadow-4: 0 14px 44px rgba(0, 0, 0, 0.12), 0 3px 9px rgba(0, 0, 0, 0.12); 281 | --vp-shadow-5: 0 18px 56px rgba(0, 0, 0, 0.16), 0 4px 12px rgba(0, 0, 0, 0.16); 282 | } 283 | 284 | /** 285 | * Z-indexes 286 | * -------------------------------------------------------------------------- */ 287 | 288 | :root { 289 | --vp-z-index-footer: 10; 290 | --vp-z-index-local-nav: 20; 291 | --vp-z-index-nav: 30; 292 | --vp-z-index-layout-top: 40; 293 | --vp-z-index-backdrop: 50; 294 | --vp-z-index-sidebar: 60; 295 | } 296 | 297 | @media (min-width: 960px) { 298 | :root { 299 | --vp-z-index-sidebar: 25; 300 | } 301 | } 302 | 303 | /** 304 | * Layouts 305 | * -------------------------------------------------------------------------- */ 306 | 307 | :root { 308 | --vp-layout-max-width: 1440px; 309 | } 310 | 311 | /** 312 | * Component: Header Anchor 313 | * -------------------------------------------------------------------------- */ 314 | 315 | :root { 316 | --vp-header-anchor-symbol: '#'; 317 | } 318 | 319 | /** 320 | * Component: Code 321 | * -------------------------------------------------------------------------- */ 322 | 323 | :root { 324 | --vp-code-line-height: 1.7; 325 | --vp-code-font-size: 0.875em; 326 | --vp-code-color: var(--vp-c-brand-1); 327 | --vp-code-link-color: var(--vp-c-brand-1); 328 | --vp-code-link-hover-color: var(--vp-c-brand-2); 329 | --vp-code-bg: var(--vp-c-default-soft); 330 | 331 | --vp-code-block-color: var(--vp-c-text-2); 332 | --vp-code-block-bg: var(--vp-c-bg-alt); 333 | --vp-code-block-divider-color: var(--vp-c-gutter); 334 | 335 | --vp-code-lang-color: var(--vp-c-text-3); 336 | 337 | --vp-code-line-highlight-color: var(--vp-c-default-soft); 338 | --vp-code-line-number-color: var(--vp-c-text-3); 339 | 340 | --vp-code-line-diff-add-color: var(--vp-c-success-soft); 341 | --vp-code-line-diff-add-symbol-color: var(--vp-c-success-1); 342 | 343 | --vp-code-line-diff-remove-color: var(--vp-c-danger-soft); 344 | --vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1); 345 | 346 | --vp-code-line-warning-color: var(--vp-c-warning-soft); 347 | --vp-code-line-error-color: var(--vp-c-danger-soft); 348 | 349 | --vp-code-copy-code-border-color: var(--vp-c-divider); 350 | --vp-code-copy-code-bg: var(--vp-c-bg-soft); 351 | --vp-code-copy-code-hover-border-color: var(--vp-c-divider); 352 | --vp-code-copy-code-hover-bg: var(--vp-c-bg); 353 | --vp-code-copy-code-active-text: var(--vp-c-text-2); 354 | --vp-code-copy-copied-text-content: 'Copied'; 355 | 356 | --vp-code-tab-divider: var(--vp-code-block-divider-color); 357 | --vp-code-tab-text-color: var(--vp-c-text-2); 358 | --vp-code-tab-bg: var(--vp-code-block-bg); 359 | --vp-code-tab-hover-text-color: var(--vp-c-text-1); 360 | --vp-code-tab-active-text-color: var(--vp-c-text-1); 361 | --vp-code-tab-active-bar-color: var(--vp-c-brand-1); 362 | } 363 | 364 | /** 365 | * Component: Button 366 | * -------------------------------------------------------------------------- */ 367 | 368 | :root { 369 | --vp-button-brand-border: transparent; 370 | --vp-button-brand-text: var(--vp-c-white); 371 | --vp-button-brand-bg: var(--vp-c-brand-3); 372 | --vp-button-brand-hover-border: transparent; 373 | --vp-button-brand-hover-text: var(--vp-c-white); 374 | --vp-button-brand-hover-bg: var(--vp-c-brand-2); 375 | --vp-button-brand-active-border: transparent; 376 | --vp-button-brand-active-text: var(--vp-c-white); 377 | --vp-button-brand-active-bg: var(--vp-c-brand-1); 378 | 379 | --vp-button-alt-border: transparent; 380 | --vp-button-alt-text: var(--vp-c-text-1); 381 | --vp-button-alt-bg: var(--vp-c-default-3); 382 | --vp-button-alt-hover-border: transparent; 383 | --vp-button-alt-hover-text: var(--vp-c-text-1); 384 | --vp-button-alt-hover-bg: var(--vp-c-default-2); 385 | --vp-button-alt-active-border: transparent; 386 | --vp-button-alt-active-text: var(--vp-c-text-1); 387 | --vp-button-alt-active-bg: var(--vp-c-default-1); 388 | 389 | --vp-button-sponsor-border: var(--vp-c-text-2); 390 | --vp-button-sponsor-text: var(--vp-c-text-2); 391 | --vp-button-sponsor-bg: transparent; 392 | --vp-button-sponsor-hover-border: var(--vp-c-sponsor); 393 | --vp-button-sponsor-hover-text: var(--vp-c-sponsor); 394 | --vp-button-sponsor-hover-bg: transparent; 395 | --vp-button-sponsor-active-border: var(--vp-c-sponsor); 396 | --vp-button-sponsor-active-text: var(--vp-c-sponsor); 397 | --vp-button-sponsor-active-bg: transparent; 398 | } 399 | 400 | /** 401 | * Component: Custom Block 402 | * -------------------------------------------------------------------------- */ 403 | 404 | :root { 405 | --vp-custom-block-font-size: 14px; 406 | --vp-custom-block-code-font-size: 13px; 407 | 408 | --vp-custom-block-info-border: transparent; 409 | --vp-custom-block-info-text: var(--vp-c-text-1); 410 | --vp-custom-block-info-bg: var(--vp-c-default-soft); 411 | --vp-custom-block-info-code-bg: var(--vp-c-default-soft); 412 | 413 | --vp-custom-block-note-border: transparent; 414 | --vp-custom-block-note-text: var(--vp-c-text-1); 415 | --vp-custom-block-note-bg: var(--vp-c-default-soft); 416 | --vp-custom-block-note-code-bg: var(--vp-c-default-soft); 417 | 418 | --vp-custom-block-tip-border: transparent; 419 | --vp-custom-block-tip-text: var(--vp-c-text-1); 420 | --vp-custom-block-tip-bg: var(--vp-c-tip-soft); 421 | --vp-custom-block-tip-code-bg: var(--vp-c-tip-soft); 422 | 423 | --vp-custom-block-important-border: transparent; 424 | --vp-custom-block-important-text: var(--vp-c-text-1); 425 | --vp-custom-block-important-bg: var(--vp-c-important-soft); 426 | --vp-custom-block-important-code-bg: var(--vp-c-important-soft); 427 | 428 | --vp-custom-block-warning-border: transparent; 429 | --vp-custom-block-warning-text: var(--vp-c-text-1); 430 | --vp-custom-block-warning-bg: var(--vp-c-warning-soft); 431 | --vp-custom-block-warning-code-bg: var(--vp-c-warning-soft); 432 | 433 | --vp-custom-block-danger-border: transparent; 434 | --vp-custom-block-danger-text: var(--vp-c-text-1); 435 | --vp-custom-block-danger-bg: var(--vp-c-danger-soft); 436 | --vp-custom-block-danger-code-bg: var(--vp-c-danger-soft); 437 | 438 | --vp-custom-block-caution-border: transparent; 439 | --vp-custom-block-caution-text: var(--vp-c-text-1); 440 | --vp-custom-block-caution-bg: var(--vp-c-caution-soft); 441 | --vp-custom-block-caution-code-bg: var(--vp-c-caution-soft); 442 | 443 | --vp-custom-block-details-border: var(--vp-custom-block-info-border); 444 | --vp-custom-block-details-text: var(--vp-custom-block-info-text); 445 | --vp-custom-block-details-bg: var(--vp-custom-block-info-bg); 446 | --vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg); 447 | } 448 | 449 | /** 450 | * Component: Input 451 | * -------------------------------------------------------------------------- */ 452 | 453 | :root { 454 | --vp-input-border-color: var(--vp-c-border); 455 | --vp-input-bg-color: var(--vp-c-bg-alt); 456 | 457 | --vp-input-switch-bg-color: var(--vp-c-default-soft); 458 | } 459 | 460 | /** 461 | * Component: Nav 462 | * -------------------------------------------------------------------------- */ 463 | 464 | :root { 465 | --vp-nav-height: 64px; 466 | --vp-nav-bg-color: var(--vp-c-bg); 467 | --vp-nav-screen-bg-color: var(--vp-c-bg); 468 | --vp-nav-logo-height: 24px; 469 | } 470 | 471 | .hide-nav { 472 | --vp-nav-height: 0px; 473 | } 474 | 475 | .hide-nav .VPSidebar { 476 | --vp-nav-height: 22px; 477 | } 478 | 479 | /** 480 | * Component: Local Nav 481 | * -------------------------------------------------------------------------- */ 482 | 483 | :root { 484 | --vp-local-nav-bg-color: var(--vp-c-bg); 485 | } 486 | 487 | /** 488 | * Component: Sidebar 489 | * -------------------------------------------------------------------------- */ 490 | 491 | :root { 492 | --vp-sidebar-width: 272px; 493 | --vp-sidebar-bg-color: var(--vp-c-bg-alt); 494 | } 495 | 496 | /** 497 | * Colors Backdrop 498 | * -------------------------------------------------------------------------- */ 499 | 500 | :root { 501 | --vp-backdrop-bg-color: rgba(0, 0, 0, 0.6); 502 | } 503 | 504 | /** 505 | * Component: Home 506 | * -------------------------------------------------------------------------- */ 507 | 508 | :root { 509 | --vp-home-hero-name-color: var(--vp-c-brand-1); 510 | --vp-home-hero-name-background: transparent; 511 | 512 | --vp-home-hero-image-background-image: none; 513 | --vp-home-hero-image-filter: none; 514 | } 515 | 516 | /** 517 | * Component: Badge 518 | * -------------------------------------------------------------------------- */ 519 | 520 | :root { 521 | --vp-badge-info-border: transparent; 522 | --vp-badge-info-text: var(--vp-c-text-2); 523 | --vp-badge-info-bg: var(--vp-c-default-soft); 524 | 525 | --vp-badge-tip-border: transparent; 526 | --vp-badge-tip-text: var(--vp-c-tip-1); 527 | --vp-badge-tip-bg: var(--vp-c-tip-soft); 528 | 529 | --vp-badge-warning-border: transparent; 530 | --vp-badge-warning-text: var(--vp-c-warning-1); 531 | --vp-badge-warning-bg: var(--vp-c-warning-soft); 532 | 533 | --vp-badge-danger-border: transparent; 534 | --vp-badge-danger-text: var(--vp-c-danger-1); 535 | --vp-badge-danger-bg: var(--vp-c-danger-soft); 536 | } 537 | 538 | /** 539 | * Component: Carbon Ads 540 | * -------------------------------------------------------------------------- */ 541 | 542 | :root { 543 | --vp-carbon-ads-text-color: var(--vp-c-text-1); 544 | --vp-carbon-ads-poweredby-color: var(--vp-c-text-2); 545 | --vp-carbon-ads-bg-color: var(--vp-c-bg-soft); 546 | --vp-carbon-ads-hover-text-color: var(--vp-c-brand-1); 547 | --vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1); 548 | } 549 | 550 | /** 551 | * Component: Local Search 552 | * -------------------------------------------------------------------------- */ 553 | 554 | :root { 555 | --vp-local-search-bg: var(--vp-c-bg); 556 | --vp-local-search-result-bg: var(--vp-c-bg); 557 | --vp-local-search-result-border: var(--vp-c-divider); 558 | --vp-local-search-result-selected-bg: var(--vp-c-bg); 559 | --vp-local-search-result-selected-border: var(--vp-c-brand-1); 560 | --vp-local-search-highlight-bg: var(--vp-c-brand-1); 561 | --vp-local-search-highlight-text: var(--vp-c-neutral-inverse); 562 | } 563 | -------------------------------------------------------------------------------- /src/utils/codeHighlighter.js: -------------------------------------------------------------------------------- 1 | import { getSingletonHighlighter } from 'shiki'; 2 | import { transformerNotationDiff } from '@shikijs/transformers'; 3 | import { escapeSvelte } from 'mdsvex'; 4 | 5 | const THEME = 'github-dark'; 6 | 7 | /** 8 | * Returns code with curly braces and backticks replaced by HTML entity equivalents 9 | * @param {string} code - highlighted HTML 10 | * @returns {string} - escaped HTML 11 | */ 12 | function escapeHtml(code) { 13 | return code.replace( 14 | /[{}`]/g, 15 | (character) => ({ '{': '{', '}': '}', '`': '`' }[character]), 16 | ); 17 | } 18 | 19 | /** 20 | * @param {string} code - code to highlight 21 | * @param {string} lang - code language 22 | * @param {string} meta - code meta 23 | * @returns {Promise} - highlighted html 24 | */ 25 | async function highlighter(code, lang, meta) { 26 | const highlighter = await getSingletonHighlighter({ 27 | langs: ['svelte', 'css', 'javascript', 'bash', 'html'], 28 | themes: ['catppuccin-latte', 'monokai'] 29 | }); 30 | const darkHtml = escapeHtml(escapeSvelte(highlighter.codeToHtml(code, { 31 | lang, 32 | theme: 'monokai', 33 | transformers: [ 34 | transformerNotationDiff() 35 | ] 36 | }))); 37 | const lightHtml = escapeHtml(escapeSvelte(highlighter.codeToHtml(code, { 38 | lang, 39 | theme: 'catppuccin-latte', 40 | transformers: [ 41 | transformerNotationDiff() 42 | ] 43 | }))); 44 | // return `{@html \`${darkHtml}${lightHtml}\` }`; 45 | return `{@html \`
${lightHtml}${darkHtml}
\` }`; 46 | // const html = shikiHighlighter.codeToHtml(code, { 47 | // lang: lang, 48 | // theme: THEME, 49 | // transformers: [ 50 | // transformerNotationDiff() 51 | // ] 52 | // }); 53 | // return escapeSvelte(html); 54 | } 55 | 56 | export default highlighter; 57 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskocik/svelty-picker/3c291e4425ea258e4d042f3eb6e1e68bd46adde0/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { mdsvex } from "mdsvex"; 2 | import adapter from '@sveltejs/adapter-auto'; 3 | import mdsvexConfig from './mdsvex.config.js'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | kit: { 8 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 9 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 10 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 11 | adapter: adapter() 12 | }, 13 | 14 | preprocess: [mdsvex(mdsvexConfig)], 15 | extensions: [".svelte", ".svelte.md"] 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /test/01-render.test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/svelte"; 2 | import { describe, it, expect } from "vitest"; 3 | import SveltyPicker from '$lib/components/SveltyPicker.svelte'; 4 | import userEvent from "@testing-library/user-event"; 5 | 6 | describe('DOM Render', async () => { 7 | it('input only', () => { 8 | const { container } = render(SveltyPicker, { props: { 9 | // value: new Date() 10 | }}); 11 | 12 | expect(container.querySelector('input[type=text]')).toBeInTheDocument(); 13 | }); 14 | 15 | 16 | it('show popup after focus', async () => { 17 | const { container } = render(SveltyPicker, { 18 | onfocus: () => console.log('focuesed') 19 | }); 20 | 21 | expect(container.querySelector('.sdt-calendar-wrap')).not.toBeInTheDocument(); 22 | 23 | const user = userEvent.setup(); 24 | await user.click(container.querySelector('input[type=text]')); 25 | 26 | expect(container.querySelector('.sdt-calendar-wrap')).toBeInTheDocument(); 27 | }); 28 | 29 | 30 | it('pickerOnly', () => { 31 | const { container } = render(SveltyPicker, { props: { 32 | pickerOnly: true 33 | }}); 34 | 35 | expect(container.querySelector('.sdt-component-wrap')).toBeInTheDocument(); 36 | expect(container.querySelector('.sdt-calendar-wrap')).toBeInTheDocument(); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test/02-datepicker.test.svelte.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | // import { render } from "vitest-browser-svelte"; 3 | import { render } from "@testing-library/svelte"; 4 | import SveltyPicker from "$lib/components/SveltyPicker.svelte"; 5 | import { userEvent } from "@testing-library/user-event"; 6 | import { test } from "vitest"; 7 | import { flushSync } from "svelte"; 8 | 9 | function sleep(ms) { 10 | return new Promise(resolve => { 11 | setTimeout(resolve, ms); 12 | }); 13 | } 14 | 15 | 16 | 17 | describe('Date selection [pickerOnly]', async () => { 18 | it('simple', async () => { 19 | let props = $state({ 20 | value: null, 21 | pickerOnly: true 22 | }); 23 | 24 | const screen = render(SveltyPicker, { props }); 25 | 26 | const user = userEvent.setup(); 27 | 28 | const day = screen.getByText('12', { selector: 'button' }); 29 | await user.click(day); 30 | 31 | // console.log(screen.container.innerHTML); 32 | 33 | expect(screen.container.querySelector('td.is-selected')).toBeInTheDocument(); 34 | expect(props.value.split('-').pop()).toEqual('12'); 35 | 36 | }); 37 | 38 | it('with initial value', async () => { 39 | let props = $state({ 40 | value: '2024-12-09', 41 | pickerOnly: true 42 | }); 43 | 44 | const screen = render(SveltyPicker, { props }); 45 | 46 | expect(screen.container.querySelector('td.is-selected').textContent).toEqual('9'); 47 | 48 | const user = userEvent.setup(); 49 | await user.click(screen.getByText('12', { exact: true })); 50 | 51 | 52 | expect(screen.container.querySelector('td.is-selected')).toBeInTheDocument(); 53 | expect(screen.container.querySelector('td.is-selected').textContent).toEqual('12'); 54 | expect(props.value).toEqual('2024-12-12'); 55 | }); 56 | 57 | 58 | it('autocommit: false', async () => { 59 | let props = $state({ 60 | value: '2024-12-09', 61 | pickerOnly: true, 62 | autocommit: false, 63 | }); 64 | 65 | const screen = render(SveltyPicker, { props }); 66 | 67 | const user = userEvent.setup(); 68 | // await user.click(screen.container.querySelector('input[type=text]')); 69 | 70 | await user.click(screen.getByText('12', { exact: true })); 71 | 72 | expect(props.value).toEqual('2024-12-09'); 73 | 74 | // expect(screen.container.querySelector('.value-dirty')).toBeInTheDocument(); 75 | 76 | await user.click(screen.getByText('Ok', { exact: true })); 77 | 78 | expect(screen.container.querySelector('td.is-selected')).toBeInTheDocument(); 79 | expect(screen.container.querySelector('td.is-selected').textContent).toEqual('12'); 80 | expect(props.value).toEqual('2024-12-12'); 81 | }); 82 | 83 | 84 | it('update value from parent', async () => { 85 | let props = $state({ 86 | value: '2024-12-09', 87 | pickerOnly: true 88 | }); 89 | 90 | const screen = render(SveltyPicker, { props }); 91 | 92 | expect(screen.container.querySelector('td.is-selected').textContent).toEqual('9'); 93 | 94 | props.value = '2024-12-12'; 95 | 96 | await sleep(500); 97 | 98 | expect(screen.container.querySelector('td.is-selected').textContent).toEqual('12'); 99 | }); 100 | }); 101 | 102 | describe('Date selection', async () => { 103 | it('simple', async () => { 104 | let props = $state({ 105 | value: null, 106 | }); 107 | 108 | const screen = render(SveltyPicker, { props }); 109 | 110 | const user = userEvent.setup(); 111 | await user.click(screen.container.querySelector('input[type=text]')); 112 | 113 | expect(props.value).toBeNull(); 114 | 115 | const day = screen.getByText('12', { selector: 'button' }); 116 | await user.click(day); 117 | 118 | expect(props.value.split('-').pop()).toEqual('12'); 119 | 120 | }); 121 | 122 | it('with initial value', async () => { 123 | let props = $state({ 124 | value: '2024-12-09', 125 | }); 126 | 127 | const screen = render(SveltyPicker, { props }); 128 | 129 | const user = userEvent.setup(); 130 | await user.click(screen.container.querySelector('input[type=text]')); 131 | 132 | expect(screen.container.querySelector('td.is-selected').textContent).toEqual('9'); 133 | 134 | await user.click(screen.getByText('12', { exact: true })); 135 | 136 | expect(props.value).toEqual('2024-12-12'); 137 | 138 | await user.click(screen.container.querySelector('input[type=text]')); 139 | 140 | expect(screen.container.querySelector('td.is-selected')).toBeInTheDocument(); 141 | expect(screen.container.querySelector('td.is-selected').textContent).toEqual('12'); 142 | }); 143 | 144 | 145 | it('autocommit: false', async () => { 146 | let props = $state({ 147 | value: '2024-12-09', 148 | autocommit: false, 149 | }); 150 | 151 | const screen = render(SveltyPicker, { props }); 152 | 153 | const user = userEvent.setup(); 154 | await user.click(screen.container.querySelector('input[type=text]')); 155 | 156 | await user.click(screen.getByText('12', { exact: true })); 157 | 158 | expect(props.value).toEqual('2024-12-09'); 159 | 160 | expect(screen.container.querySelector('.value-dirty')).toBeInTheDocument(); 161 | 162 | await user.click(screen.getByText('Ok', { exact: true })); 163 | 164 | flushSync(); 165 | 166 | // popup should be closed, but is not in jsdom 🤷 TODO: use vite-browser here 167 | // expect(screen.container.querySelector('.sdt-calendar-wrap')).not.toBeInTheDocument(); 168 | // expect(screen.container.querySelector('td.is-selected')).not.toBeInTheDocument(); 169 | 170 | expect(props.value).toEqual('2024-12-12'); 171 | }); 172 | 173 | 174 | it('update value from parent', async () => { 175 | let props = $state({ 176 | value: '2024-12-09', 177 | }); 178 | 179 | const screen = render(SveltyPicker, { props }); 180 | 181 | const user = userEvent.setup(); 182 | await user.click(screen.container.querySelector('input[type=text]')); 183 | 184 | expect(screen.container.querySelector('td.is-selected').textContent).toEqual('9'); 185 | 186 | props.value = '2024-12-12'; 187 | 188 | await sleep(500); 189 | 190 | expect(screen.container.querySelector('td.is-selected').textContent).toEqual('12'); 191 | }); 192 | }); 193 | 194 | 195 | describe('formatting', () => { 196 | it('simple format test', async () => { 197 | let props = $state({ 198 | value: '2024-12-09', 199 | displayFormat: 'yyyy.mm.dd', 200 | autocommit: false, 201 | }); 202 | 203 | const screen = render(SveltyPicker, { props }); 204 | 205 | expect(screen.container.querySelector('input[type=hidden]').value).toEqual('2024-12-09'); 206 | expect(screen.container.querySelector('input[type=text]').value).toEqual('2024.12.09'); 207 | }); 208 | 209 | it('update [format]', async () => { 210 | let props = $state({ 211 | value: '2024-12-09', 212 | autocommit: false, 213 | }); 214 | 215 | const screen = render(SveltyPicker, { props }); 216 | 217 | screen.rerender({ 218 | format: 'dd.mm.yyyy' 219 | }); 220 | 221 | await sleep(500); 222 | 223 | expect(screen.container.querySelector('input[type=hidden]').value).toEqual('09.12.2024'); 224 | expect(screen.container.querySelector('input[type=text]').value).toEqual('09.12.2024'); 225 | }); 226 | 227 | it('update [displayFormat]', async () => { 228 | let props = $state({ 229 | value: '2024-12-09', 230 | autocommit: false, 231 | }); 232 | 233 | const screen = render(SveltyPicker, { props }); 234 | 235 | screen.rerender({ 236 | displayFormat: 'dd.mm.yyyy' 237 | }); 238 | 239 | await sleep(500); 240 | 241 | expect(screen.container.querySelector('input[type=hidden]').value).toEqual('2024-12-09'); 242 | expect(screen.container.querySelector('input[type=text]').value).toEqual('09.12.2024'); 243 | }); 244 | }) 245 | 246 | test('clear', async () => { 247 | 248 | let props = $state({ 249 | value: '10:24', 250 | mode: 'time', 251 | format: 'hh:ii', 252 | pickerOnly: true 253 | }); 254 | const screen = render(SveltyPicker, { props }); 255 | 256 | const user = userEvent.setup(); 257 | await user.click(screen.getByText('Clear')); 258 | 259 | expect(props.value || null).toEqual(null); 260 | }); 261 | -------------------------------------------------------------------------------- /test/03-timepicker.test.svelte.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | // import { render } from "vitest-browser-svelte"; 3 | import { render } from "@testing-library/svelte"; 4 | import SveltyPicker from "$lib/components/SveltyPicker.svelte"; 5 | import { userEvent } from "@testing-library/user-event"; 6 | 7 | function sleep(ms) { 8 | return new Promise(resolve => { 9 | setTimeout(resolve, ms); 10 | }); 11 | } 12 | 13 | describe('Time selection', async () => { 14 | it('simple', async () => { 15 | let props = $state({ 16 | value: null, 17 | format: 'hh:ii' 18 | }); 19 | 20 | const screen = render(SveltyPicker, { props }); 21 | 22 | const mainInput = screen.container.querySelector('input[type=text]'); 23 | 24 | const user = userEvent.setup(); 25 | await user.click(mainInput); 26 | 27 | const hour = screen.getByText('12', { selector: 'button.sdt-tick' }); 28 | await user.click(hour); 29 | 30 | const minute = screen.getByText('30', { selector: 'button.sdt-tick' }); 31 | await user.click(minute); 32 | 33 | 34 | // popup should be closed, but is not in jsdom 🤷 TODO: use vite-browser here 35 | // expect(screen.container.querySelector('.sdt-calendar-wrap')).not.toBeInTheDocument(); 36 | expect(props.value).toEqual('12:30'); 37 | 38 | }); 39 | }); 40 | 41 | 42 | describe('Time selection [pickerOnly]', async () => { 43 | it('simple', async () => { 44 | let props = $state({ 45 | value: null, 46 | format: 'hh:ii', 47 | pickerOnly: true 48 | }); 49 | 50 | const screen = render(SveltyPicker, { props }); 51 | 52 | const user = userEvent.setup(); 53 | 54 | const hour = screen.getByText('12', { selector: 'button.sdt-tick' }); 55 | await user.click(hour); 56 | 57 | const minute = screen.getByText('30', { selector: 'button.sdt-tick' }); 58 | await user.click(minute); 59 | 60 | expect(minute.classList.contains('is-selected')).toBeTruthy(); 61 | expect(screen.container.querySelector('button.is-selected[data-value="30"]')).toBeInTheDocument(); 62 | expect(props.value).toEqual('12:30'); 63 | 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/04-datetime.test.svelte.js: -------------------------------------------------------------------------------- 1 | import { test } from "vitest"; 2 | 3 | test.skip('datetime tests', () => { 4 | // TODO: 5 | }); 6 | -------------------------------------------------------------------------------- /test/05-daterange.test.svelte.js: -------------------------------------------------------------------------------- 1 | import { test } from "vitest"; 2 | 3 | test.skip('daterange tests', () => { 4 | // TODO: 5 | }); 6 | -------------------------------------------------------------------------------- /test/vitest-setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | import { vi } from 'vitest'; 3 | 4 | // mocking Element.animate 5 | Element.prototype.animate = vi 6 | .fn() 7 | .mockImplementation(() => ({ cancel: vi.fn(), finished: Promise.resolve() })); 8 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import {svelteTesting} from '@testing-library/svelte/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit(), svelteTesting()], 7 | optimizeDeps: { 8 | include: ['@floating-ui/dom'] 9 | }, 10 | test: { 11 | environment: 'jsdom', 12 | include: ['test/**/*.{test,spec,test.svelte}.{js,ts}'], 13 | setupFiles: ['test/vitest-setup.js'], 14 | // browser: { 15 | // enabled: true, 16 | // provider: 'playwright', 17 | // name: 'firefox', 18 | // } 19 | } 20 | }); 21 | --------------------------------------------------------------------------------