├── .github ├── FUNDING.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── README.md ├── docs ├── adfree.html ├── css │ ├── style.css │ └── style.css.map ├── favicon.ico ├── favicon.svg ├── images │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── chatgpt-api-playground.jpg │ ├── default-image-thumbnail.jpg │ ├── default-image.jpg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── favicon.png │ ├── favicon.svg │ ├── featured.jpeg │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ └── safari-pinned-tab.svg ├── index.html ├── js │ ├── classes.js │ ├── cryptography.js │ ├── export.js │ ├── index.js │ ├── manageLocalStorage.js │ ├── openAI.js │ ├── templates.js │ ├── utils.js │ └── vendor │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── crypto-js.min.js │ │ └── marked.min.js └── site.webmanifest ├── package-lock.json ├── package.json ├── scripts └── generateAdFree.js ├── src ├── scss │ ├── _main.scss │ ├── _vendor.scss │ ├── bootstrap │ │ ├── _accordion.scss │ │ ├── _alert.scss │ │ ├── _badge.scss │ │ ├── _breadcrumb.scss │ │ ├── _button-group.scss │ │ ├── _buttons.scss │ │ ├── _card.scss │ │ ├── _carousel.scss │ │ ├── _close.scss │ │ ├── _containers.scss │ │ ├── _dropdown.scss │ │ ├── _forms.scss │ │ ├── _functions.scss │ │ ├── _grid.scss │ │ ├── _helpers.scss │ │ ├── _images.scss │ │ ├── _list-group.scss │ │ ├── _maps.scss │ │ ├── _mixins.scss │ │ ├── _modal.scss │ │ ├── _nav.scss │ │ ├── _navbar.scss │ │ ├── _offcanvas.scss │ │ ├── _pagination.scss │ │ ├── _placeholders.scss │ │ ├── _popover.scss │ │ ├── _progress.scss │ │ ├── _reboot.scss │ │ ├── _root.scss │ │ ├── _spinners.scss │ │ ├── _tables.scss │ │ ├── _toasts.scss │ │ ├── _tooltip.scss │ │ ├── _transitions.scss │ │ ├── _type.scss │ │ ├── _utilities.scss │ │ ├── _variables-dark.scss │ │ ├── _variables.scss │ │ ├── bootstrap-grid.scss │ │ ├── bootstrap-reboot.scss │ │ ├── bootstrap-utilities.scss │ │ ├── bootstrap.scss │ │ ├── forms │ │ │ ├── _floating-labels.scss │ │ │ ├── _form-check.scss │ │ │ ├── _form-control.scss │ │ │ ├── _form-range.scss │ │ │ ├── _form-select.scss │ │ │ ├── _form-text.scss │ │ │ ├── _input-group.scss │ │ │ ├── _labels.scss │ │ │ └── _validation.scss │ │ ├── helpers │ │ │ ├── _clearfix.scss │ │ │ ├── _color-bg.scss │ │ │ ├── _colored-links.scss │ │ │ ├── _position.scss │ │ │ ├── _ratio.scss │ │ │ ├── _stacks.scss │ │ │ ├── _stretched-link.scss │ │ │ ├── _text-truncation.scss │ │ │ ├── _visually-hidden.scss │ │ │ └── _vr.scss │ │ ├── mixins │ │ │ ├── _alert.scss │ │ │ ├── _backdrop.scss │ │ │ ├── _banner.scss │ │ │ ├── _border-radius.scss │ │ │ ├── _box-shadow.scss │ │ │ ├── _breakpoints.scss │ │ │ ├── _buttons.scss │ │ │ ├── _caret.scss │ │ │ ├── _clearfix.scss │ │ │ ├── _color-mode.scss │ │ │ ├── _color-scheme.scss │ │ │ ├── _container.scss │ │ │ ├── _deprecate.scss │ │ │ ├── _forms.scss │ │ │ ├── _gradients.scss │ │ │ ├── _grid.scss │ │ │ ├── _image.scss │ │ │ ├── _list-group.scss │ │ │ ├── _lists.scss │ │ │ ├── _pagination.scss │ │ │ ├── _reset-text.scss │ │ │ ├── _resize.scss │ │ │ ├── _table-variants.scss │ │ │ ├── _text-truncate.scss │ │ │ ├── _transition.scss │ │ │ ├── _utilities.scss │ │ │ └── _visually-hidden.scss │ │ ├── utilities │ │ │ └── _api.scss │ │ └── vendor │ │ │ └── _rfs.scss │ └── style.scss └── ts │ ├── classes.ts │ ├── cryptography.ts │ ├── export.ts │ ├── index.ts │ ├── manageLocalStorage.ts │ ├── openAI.ts │ ├── templates.ts │ └── utils.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kannansuresh 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: aneejian 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 | custom: ['https://www.buymeacoffee.com/aneejian', 'https://donorbox.org/change-case-excel-add-in-donation'] 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | name: Node.js CI 4 | on: 5 | workflow_dispatch: 6 | push: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [18.x] 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | cache: 'npm' 20 | - run: npm install 21 | - run: npm run build --if-present 22 | - name: setup git config 23 | run: | 24 | git config user.name "GitHub Actions Bot" 25 | git config user.email "<>" 26 | - name: commit and push 27 | run: | 28 | git add --all 29 | git commit -m "Updated files." || echo "No changes to commit." 30 | git push || echo "No changes to push." 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cache files 2 | .eslintcache 3 | .npm 4 | *.tsbuildinfo 5 | # Directories 6 | node_modules/ 7 | # Env files # A good safeguard against accidentally publishing 8 | .env. 9 | .env.test 10 | # Logs 11 | *.log 12 | logs 13 | npm-debug.log* 14 | # Misc files 15 | .DS_Store 16 | notes.md 17 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT API Playground 2 | 3 | [![Node.js CI](https://github.com/kannansuresh/chatgpt-playground/actions/workflows/nodejs.yml/badge.svg)](https://github.com/kannansuresh/chatgpt-playground/actions/workflows/nodejs.yml) 4 | 5 | [![pages-build-deployment](https://github.com/kannansuresh/chatgpt-playground/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/kannansuresh/chatgpt-playground/actions/workflows/pages/pages-build-deployment) 6 | 7 | Just like the ChatGPT Playground, this tool helps you in playing with the ChatGPT model. It is a simple web app that lets you have a chat with ChatGPT using the API. 8 | 9 | I suggest you to read our article on [Getting Started with ChatGPT API](https://aneejian.com/getting-started-chat-gpt-api-comprehensive-guide/) before using this tool. That will give you a good idea of what these fields represent. 10 | 11 | I suggest you to read our article on [Getting Started with ChatGPT API](https://aneejian.com/getting-started-chat-gpt-api-comprehensive-guide/) before using this tool. That will give you a good idea of what these fields represent. 12 | 13 | ## Demo 14 | 15 | ### [Demo Video](https://www.youtube.com/v/OYQ7Wfa6uU4) 16 | 17 | ### Features 18 | 19 | 1. Provide prompts as System, User, or Assistant. 20 | 2. Switch the roles of each message. 21 | 3. Add new messages. 22 | 4. Delete any irrelevant messages. 23 | 5. Export the chat as Markdown file. 24 | 6. Export the chat as HTML file. 25 | 7. Export the code that generated the chat as a Python file. 26 | 27 | ### Future plans 28 | 29 | - [x] Create a standalone tool and make the code open source. 30 | - [x] Implement HTML preview of responses. 31 | - [ ] Add more API settings like temperature, top_p, and max_tokens. 32 | - [ ] Add more programming language exports like JavaScript, C#, and PHP. 33 | - [ ] Option to save chats to local and load it later to continue conversation. 34 | 35 | ### FAQ 36 | 37 | #### How to get API Key? 38 | 39 | You can get API Key by visiting [platform.openai.com](https://platform.openai.com/account/api-keys) and clicking on "Get API Key" button. 40 | 41 | #### How does save API key work? 42 | 43 | API Key get stored in your browser's local storage. This means that it is stored in your browser and not on our site. The key is stored in an encrypted format and is not accessible to us. 44 | 45 | #### Can you access the chats I make? 46 | 47 | No, we cannot access the chats you make. This tool just acts as an interface for ChatGPT API. We do not have access or store any of your chats. 48 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/favicon.ico -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/images/chatgpt-api-playground.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/chatgpt-api-playground.jpg -------------------------------------------------------------------------------- /docs/images/default-image-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/default-image-thumbnail.jpg -------------------------------------------------------------------------------- /docs/images/default-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/default-image.jpg -------------------------------------------------------------------------------- /docs/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/favicon-16x16.png -------------------------------------------------------------------------------- /docs/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/favicon-32x32.png -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/favicon.ico -------------------------------------------------------------------------------- /docs/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/favicon.png -------------------------------------------------------------------------------- /docs/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/images/featured.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/featured.jpeg -------------------------------------------------------------------------------- /docs/images/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/mstile-144x144.png -------------------------------------------------------------------------------- /docs/images/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/mstile-150x150.png -------------------------------------------------------------------------------- /docs/images/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/mstile-310x150.png -------------------------------------------------------------------------------- /docs/images/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/mstile-310x310.png -------------------------------------------------------------------------------- /docs/images/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kannansuresh/chatgpt-playground/e454bf53d57aed2b05b7d132305acd5e15f0386f/docs/images/mstile-70x70.png -------------------------------------------------------------------------------- /docs/images/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/js/classes.js: -------------------------------------------------------------------------------- 1 | export class payloadRole { 2 | constructor(role, icon, short, placeholder) { 3 | this.role = role; 4 | this.icon = icon; 5 | this.short = short; 6 | this.placeholder = placeholder; 7 | } 8 | } 9 | export class payloadMessage { 10 | constructor(role, content) { 11 | this.role = role; 12 | this.content = content; 13 | } 14 | } 15 | export class chatGPT { 16 | constructor(...args) { 17 | this.model = 'gpt-3.5-turbo'; 18 | this.stream = true; 19 | this.endPoint = 'https://api.openai.com/v1/chat/completions'; 20 | this.apiKey = ''; 21 | if (args.length === 0) { 22 | this.apiKey = ''; 23 | this.payloadMessages = []; 24 | } 25 | else if (args.length === 1) { 26 | this.apiKey = args[0]; 27 | this.payloadMessages = []; 28 | } 29 | else if (args.length === 2) { 30 | this.apiKey = args[0]; 31 | this.payloadMessages = args[1]; 32 | } 33 | else { 34 | this.model = args[0]; 35 | this.stream = args[1]; 36 | this.endPoint = args[2]; 37 | this.payloadMessages = args[3]; 38 | } 39 | } 40 | getRequestData() { 41 | return { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | Authorization: `Bearer ${this.apiKey}`, 46 | }, 47 | body: JSON.stringify({ 48 | model: this.model, 49 | messages: this.payloadMessages, 50 | stream: this.stream, 51 | }), 52 | }; 53 | } 54 | } 55 | chatGPT.roles = { 56 | system: new payloadRole('system', '🧠', 'sys', ''), 57 | user: new payloadRole('user', '👤', 'usr', 'Enter a user message here.'), 58 | assistant: new payloadRole('assistant', '🤖', 'ast', 'Enter an assistant message here.'), 59 | }; 60 | -------------------------------------------------------------------------------- /docs/js/cryptography.js: -------------------------------------------------------------------------------- 1 | export function encryptDecrypt(plainOrEncryptedString, performEncryption = false) { 2 | try { 3 | const userHash = generateUserHash().toString(); 4 | if (performEncryption) 5 | return CryptoJS.AES.encrypt(plainOrEncryptedString, userHash).toString(); 6 | return CryptoJS.AES.decrypt(plainOrEncryptedString, userHash).toString(CryptoJS.enc.Utf8); 7 | } 8 | catch (error) { 9 | return plainOrEncryptedString; 10 | } 11 | } 12 | export function encrypt(plainString) { 13 | const userHash = generateUserHash().toString(); 14 | return CryptoJS.AES.encrypt(plainString, userHash).toString(); 15 | } 16 | export function decrypt(encryptedString) { 17 | const userHash = generateUserHash().toString(); 18 | return CryptoJS.AES.decrypt(encryptedString, userHash).toString(CryptoJS.enc.Utf8); 19 | } 20 | // Generate a hash based on the user's browser and machine properties 21 | function generateUserHash() { 22 | const userAgent = navigator.userAgent; 23 | const platform = userAgent.indexOf('Win') !== -1 24 | ? 'Windows' 25 | : userAgent.indexOf('Mac') !== -1 26 | ? 'MacOS' 27 | : userAgent.indexOf('X11') !== -1 28 | ? 'UNIX' 29 | : userAgent.indexOf('Linux') !== -1 30 | ? 'Linux' 31 | : 'Unknown OS'; 32 | const hashInput = userAgent + platform; 33 | const hash = CryptoJS.SHA256(hashInput); // use a cryptographic hash function (e.g. SHA-256) to generate a hash 34 | return hash; 35 | } 36 | -------------------------------------------------------------------------------- /docs/js/export.js: -------------------------------------------------------------------------------- 1 | import * as templates from './templates.js'; 2 | import * as utils from './utils.js'; 3 | const htmlTemplate = templates.html; 4 | const pythonTemplate = templates.python; 5 | const brand = { 6 | publisher: 'Aneejian', 7 | productName: 'ChatGPT Playground', 8 | url: 'https://aneejian.com/chatgpt-playground/', 9 | }; 10 | const brandLineMd = `Downloaded from [${brand.publisher} ${brand.productName}](${brand.url})`; 11 | function getChats() { 12 | const textAreas = document.querySelectorAll('textarea'); 13 | if (!textAreas.length) { 14 | utils.showModal('Message Export', 'No messages to download.', ''); 15 | return ''; 16 | } 17 | const text = Array.from(textAreas) 18 | .map(t => { 19 | const value = t.value.trim(); 20 | if (!value) 21 | return ''; 22 | const roleType = t.parentElement?.querySelector('button')?.getAttribute('data-role-type')?.toUpperCase() || 'UNKNOWN'; 23 | return `**${roleType}**\n\n${value}\n\n---\n\n`; 24 | }) 25 | .join(''); 26 | return text; 27 | } 28 | export function downloadMarkdown() { 29 | const text = getChats(); 30 | if (!text?.trim()) { 31 | utils.showModal('Markdown Export', 'No messages to download. Enter at least one prompt.', ''); 32 | return; 33 | } 34 | const { dateString, timeString } = utils.getDateTimeStrings(); 35 | const filename = `Aneejian-ChatGPT-Playground-${dateString}-${timeString}.md`; 36 | const markdownText = `# Aneejian - ChatGPT Playground Export\n\n${text}\n\n${brandLineMd} on ${dateString} at ${timeString}`; 37 | utils.createDownloadLink(filename, markdownText, 'text/plain'); 38 | } 39 | export function downloadHTML() { 40 | let text = getChats(); 41 | if (!text?.trim()) { 42 | utils.showModal('HTML Export', 'No messages to download. Enter at least one prompt.', ''); 43 | return; 44 | } 45 | const { dateString, timeString } = utils.getDateTimeStrings(); 46 | const filename = `Aneejian-ChatGPT-Playground-${dateString}-${timeString}.html`; 47 | // @ts-ignore 48 | text = marked.parse(`${text}\n\n${brandLineMd} on ${dateString} at ${timeString}`); 49 | text = htmlTemplate.replace('', text); 50 | utils.createDownloadLink(filename, text, 'text/html'); 51 | } 52 | export function downloadPython(messages, model) { 53 | if (!messages.length) { 54 | utils.showModal('Python Export', 'No messages to download. Enter at least one prompt.', ''); 55 | return; 56 | } 57 | const pythonCode = pythonTemplate.replace('', model).replace('', JSON.stringify(messages)); 58 | const { dateString, timeString } = utils.getDateTimeStrings(); 59 | const filename = `Aneejian-ChatGPT-Playground-${dateString}-${timeString}.py`; 60 | utils.createDownloadLink(filename, pythonCode, 'text/html'); 61 | } 62 | -------------------------------------------------------------------------------- /docs/js/manageLocalStorage.js: -------------------------------------------------------------------------------- 1 | import * as crypto from './cryptography.js'; 2 | const LOCAL_STORAGE_API_KEY = 'chatGPTPlaygroundAPIKey'; 3 | const LOCAL_STORAGE_SAVE_KEY = 'chatGPTPlaygroundSaveAPIChoice'; 4 | const LOCAL_STORAGE_MODEL_KEY = 'chatGPTPlaygroundModel'; 5 | // Get the API key from local storage 6 | export function getAPIKey() { 7 | const encryptedString = localStorage.getItem(LOCAL_STORAGE_API_KEY); 8 | if (encryptedString) { 9 | try { 10 | const decryptedString = crypto.decrypt(encryptedString); 11 | return decryptedString; 12 | } 13 | catch (error) { 14 | console.log('Error decrypting API key: ' + error); 15 | return ''; 16 | } 17 | } 18 | } 19 | // Save the API key to local storage 20 | export function setAPIKey(apiKey) { 21 | const encryptedString = crypto.encrypt(apiKey); 22 | localStorage.setItem(LOCAL_STORAGE_API_KEY, encryptedString); 23 | } 24 | // Save the model to local storage 25 | export function setModel(model) { 26 | localStorage.setItem(LOCAL_STORAGE_MODEL_KEY, model); 27 | } 28 | // Get the model from local storage 29 | export function getModel() { 30 | const model = localStorage.getItem(LOCAL_STORAGE_MODEL_KEY); 31 | if (model) 32 | return model; 33 | } 34 | // Get the user's choice to save the API key to local storage 35 | export function getSaveAPIChoice() { 36 | const saveAPIKeyChoice = localStorage.getItem(LOCAL_STORAGE_SAVE_KEY); 37 | if (saveAPIKeyChoice) 38 | return saveAPIKeyChoice === 'true'; 39 | return false; 40 | } 41 | // Save the user's choice to save the API key to local storage 42 | export function setSaveAPIChoice(saveAPIKeyChoice) { 43 | localStorage.setItem(LOCAL_STORAGE_SAVE_KEY, saveAPIKeyChoice.toString()); 44 | if (!saveAPIKeyChoice) { 45 | localStorage.removeItem(LOCAL_STORAGE_API_KEY); 46 | } 47 | } 48 | export function initializeForm(checkbox, inputField) { 49 | const saveChoice = getSaveAPIChoice(); 50 | checkbox.checked = saveChoice; 51 | if (saveChoice) { 52 | const savedKey = getAPIKey(); 53 | if (savedKey) 54 | inputField.value = savedKey; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docs/js/openAI.js: -------------------------------------------------------------------------------- 1 | import { chatGPT } from './classes.js'; 2 | import { resizeTextarea, getPreviewHtml } from './utils.js'; 3 | let reader; 4 | export function stopStream() { 5 | console.log('Reader is: ' + reader); 6 | if (reader) { 7 | reader.cancel(); 8 | } 9 | } 10 | export async function openAIChatComplete(gptData, textArea) { 11 | const previewDiv = textArea.parentElement?.querySelector('.preview'); 12 | const url = gptData.endPoint; 13 | const requestData = gptData.getRequestData(); 14 | let response; 15 | try { 16 | response = await fetch(url, requestData); 17 | // check for response errors 18 | if (!response.ok) { 19 | const error = await response.json(); 20 | throw new Error(`${error.error.code}\n${error.error.message}`); 21 | } 22 | reader = response.body?.getReader(); 23 | let responseText = ''; 24 | const onData = (chunk) => { 25 | const textDecoder = new TextDecoder(); 26 | const jsonString = textDecoder.decode(chunk, { stream: true }); 27 | let jsonStrings = jsonString.split('data:'); 28 | jsonStrings = jsonStrings.map(str => { 29 | if (str.includes('[DONE]')) { 30 | return str.replace('[DONE]', ''); 31 | } 32 | return str; 33 | }); 34 | jsonStrings = jsonStrings.map(str => str.trim()).filter(str => str.length > 0); 35 | textArea.classList.remove('hidden'); 36 | previewDiv.classList.add('hidden'); 37 | for (let i = 0; i < jsonStrings.length; i++) { 38 | const responseData = JSON.parse(jsonStrings[i]); 39 | const choices = responseData.choices; 40 | if (choices && choices.length > 0) { 41 | const delta = choices[0].delta; 42 | if (delta && delta.content) { 43 | const content = delta.content; 44 | responseText += content; 45 | updateTextAreaAndPreview(textArea, previewDiv, content); 46 | } 47 | } 48 | } 49 | }; 50 | const onDone = () => { 51 | updateTextAreaAndPreview(textArea, previewDiv, responseText, true); 52 | }; 53 | const read = () => { 54 | return reader?.read().then(({ done, value }) => { 55 | if (done) { 56 | onDone(); 57 | return; 58 | } 59 | onData(value); 60 | return read(); 61 | }); 62 | }; 63 | await read(); 64 | return { result: true, response: responseText.trim() }; 65 | } 66 | catch (error) { 67 | const errorMsg = `${error}`; 68 | updateTextAreaAndPreview(textArea, previewDiv, errorMsg, true, true); 69 | console.log(errorMsg); 70 | return { result: false, response: errorMsg }; 71 | } 72 | finally { 73 | textArea.placeholder = chatGPT.roles['assistant'].placeholder; 74 | } 75 | } 76 | function updateTextAreaAndPreview(textArea, previewDiv, text, responseComplete = false, error = false) { 77 | textArea.value += text; 78 | textArea.value = textArea.value.trimStart(); 79 | // @ts-ignore 80 | previewDiv.innerHTML = getPreviewHtml(textArea.value); 81 | resizeTextarea(textArea); 82 | textArea.scrollHeight; 83 | if (responseComplete) { 84 | textArea.value = error ? text : text.trim(); 85 | // @ts-ignore 86 | previewDiv.innerHTML = getPreviewHtml(textArea.value); 87 | resizeTextarea(textArea); 88 | textArea.classList.add('hidden'); 89 | previewDiv.classList.remove('hidden'); 90 | } 91 | } 92 | export default openAIChatComplete; 93 | -------------------------------------------------------------------------------- /docs/js/templates.js: -------------------------------------------------------------------------------- 1 | export const html = ` 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Aneejian ChatGPT Playground Export 10 | 11 | 12 |
13 |
14 |

Aneejian ChatGPT Playground Export

15 | 16 |
17 |
18 | 19 | `; 20 | export const python = ` 21 | # code generated by Aneejian's ChatGPT Playground 22 | # https://aneejian.com/chatgpt-api-playground 23 | import openai 24 | API_KEY = 'you api key here' 25 | openai.api_key = API_KEY 26 | completion = openai.ChatCompletion.create( 27 | model="", 28 | messages= 29 | )`; 30 | -------------------------------------------------------------------------------- /docs/js/utils.js: -------------------------------------------------------------------------------- 1 | // import { stopStream } from './openAI'; 2 | export function resizeTextarea(textarea) { 3 | textarea.style.height = 'auto'; 4 | textarea.style.height = `${textarea.scrollHeight}px`; 5 | textarea.rows = textarea.value.split('\n').length > 1 ? textarea.value.split('\n').length : 1; 6 | ensureButtonInView(); 7 | } 8 | function ensureButtonInView() { 9 | const button = document.getElementById('stopGenerationBtn'); 10 | if (!button) 11 | return; 12 | const buttonRect = button.getBoundingClientRect(); 13 | const inViewPort = buttonRect.top >= 0 && buttonRect.left >= 0 && buttonRect.bottom <= window.innerHeight && buttonRect.right <= window.innerWidth; 14 | if (!inViewPort) { 15 | button.scrollIntoView({ behavior: 'smooth', block: 'center' }); // scroll to element 16 | } 17 | } 18 | export function deleteMessage(messageToDelete) { 19 | try { 20 | messageToDelete.parentElement?.remove(); 21 | } 22 | catch (err) { 23 | console.error('Error deleting message:', err); 24 | } 25 | } 26 | export function disableOrEnableElements(disable = true) { 27 | const buttons = document.querySelectorAll('button'); 28 | const textAreas = document.querySelectorAll('textarea'); 29 | const elements = [...buttons, ...textAreas]; 30 | const filteredElements = Array.from(elements).filter(element => !element.classList.contains('is-disabled')); 31 | filteredElements.forEach(element => { 32 | element.disabled = disable; 33 | }); 34 | } 35 | export function addSpinner(messagesContainer) { 36 | disableOrEnableElements(true); 37 | const placeholderDiv = document.createElement('div'); 38 | placeholderDiv.id = 'placeholderDiv'; 39 | const stopGeneratingButton = document.createElement('button'); 40 | stopGeneratingButton.className = 'btn btn-danger btn-sm mb-2 mt-2'; 41 | stopGeneratingButton.textContent = 'Stop Generating'; 42 | stopGeneratingButton.style.display = 'block'; 43 | stopGeneratingButton.type = 'button'; 44 | stopGeneratingButton.id = 'stopGenerationBtn'; 45 | const loadingParagraph = document.createElement('p'); 46 | loadingParagraph.textContent = 'Fetching response'; 47 | loadingParagraph.className = 'loading'; 48 | placeholderDiv.appendChild(loadingParagraph); 49 | placeholderDiv.appendChild(stopGeneratingButton); 50 | messagesContainer.appendChild(placeholderDiv); 51 | return placeholderDiv; 52 | } 53 | export function removeSpinner() { 54 | const spinnerDiv = document.getElementById('placeholderDiv'); 55 | if (spinnerDiv) 56 | spinnerDiv.remove(); 57 | disableOrEnableElements(false); 58 | } 59 | export function getDateTimeStrings() { 60 | const now = new Date(); 61 | const dateOptions = { year: 'numeric', month: '2-digit', day: '2-digit' }; 62 | const timeOptions = { 63 | hour: '2-digit', 64 | minute: '2-digit', 65 | second: '2-digit', 66 | hour12: false, 67 | }; 68 | // @ts-ignore 69 | const dateString = now.toLocaleDateString(undefined, dateOptions).replace(/\//g, '-'); 70 | // @ts-ignore 71 | const timeString = now.toLocaleTimeString(undefined, timeOptions).replace(/:/g, '-'); 72 | return { dateString, timeString }; 73 | } 74 | export function createDownloadLink(filename, data, type) { 75 | const blob = new Blob([data], { 76 | type, 77 | }); 78 | const url = window.URL.createObjectURL(blob); 79 | const a = document.createElement('a'); 80 | a.href = url; 81 | a.download = filename; 82 | a.click(); 83 | window.URL.revokeObjectURL(url); 84 | a.remove(); 85 | } 86 | export function showModal(titleString = '', bodyString = '', buttonString = '', closeButtonString = 'Close', buttonFunction = null) { 87 | const title = document.getElementById('modalTitle'); 88 | const body = document.getElementById('modalBody'); 89 | const button = document.getElementById('modalButton'); 90 | const closeButton = document.getElementById('modalCloseButton'); 91 | title.textContent = titleString; 92 | body.innerHTML = bodyString; 93 | button.textContent = buttonString; 94 | closeButton.textContent = closeButtonString || 'Close'; 95 | if (!buttonString) { 96 | button.style.display = 'none'; 97 | } 98 | else { 99 | button.style.display = 'block'; 100 | if (buttonFunction != null) { 101 | button.addEventListener('click', e => { 102 | buttonFunction('adfree.html'); 103 | }); 104 | } 105 | } 106 | // @ts-ignore 107 | const myModal = new bootstrap.Modal(document.getElementById('modal')); 108 | // @ts-ignore 109 | myModal.show(); 110 | } 111 | // function to navigate to a url 112 | export const navigateTo = (url) => { 113 | window.location.href = url; 114 | }; 115 | export function getPreviewHtml(text) { 116 | // const regex = /(?'); 118 | // @ts-ignore 119 | return marked.parse(text); 120 | } 121 | -------------------------------------------------------------------------------- /docs/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChatGPT Play Ground | Aneejian", 3 | "short_name": "ChatGPT Play Ground | Aneejian", 4 | "description": "ChatGPT Play Ground - run ChatGPT with your API key.", 5 | "start_url": "/", 6 | "icons": [ 7 | { 8 | "src": "/android-chrome-192x192.png", 9 | "sizes": "192x192", 10 | "type": "image/png" 11 | }, 12 | { 13 | "src": "/android-chrome-512x512.png", 14 | "sizes": "512x512", 15 | "type": "image/png" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-playground", 3 | "version": "0.0.1", 4 | "description": "A playground for ChatGPT API.", 5 | "main": "index.ts", 6 | "scripts": { 7 | "build": "tsc && npm run scss && npm run generateAdFree", 8 | "dev": "npm-run-all --parallel live tscWatch scssWatch", 9 | "compile": "tsc && scss", 10 | "live": "live-server --port=3000 --open=docs/index.html", 11 | "scssWatch": "sass --watch src/scss/style.scss:docs/css/style.css", 12 | "scss": "sass src/scss/style.scss:docs/css/style.css", 13 | "tscWatch": "tsc -w", 14 | "generateAdFree": "node scripts/generateAdFree.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/kannansuresh/chatgpt-playground.git" 19 | }, 20 | "keywords": [ 21 | "ChatGPT", 22 | "API" 23 | ], 24 | "author": "Kannan Suresh", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/kannansuresh/chatgpt-playground/issues" 28 | }, 29 | "homepage": "https://github.com/kannansuresh/chatgpt-playground#readme", 30 | "devDependencies": { 31 | "@types/crypto-js": "^4.1.1", 32 | "@types/marked": "^4.0.8", 33 | "fs": "^0.0.1-security", 34 | "npm-run-all": "^4.1.5", 35 | "sass": "^1.59.2", 36 | "typescript": "^4.9.5" 37 | }, 38 | "dependencies": { 39 | "crypto-js": "^4.1.1", 40 | "marked": "^4.2.12" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scripts/generateAdFree.js: -------------------------------------------------------------------------------- 1 | const { existsSync, unlinkSync, readFileSync, writeFileSync, createReadStream, createWriteStream } = require('fs'); 2 | const path = require('path'); 3 | 4 | const rootDirPath = path.resolve(__dirname, '..'); 5 | const inputFilePath = path.resolve(rootDirPath, 'docs', 'index.html'); 6 | const outputFilePath = path.resolve(rootDirPath, 'docs', 'adfree.html'); 7 | 8 | if (existsSync(outputFilePath)) { 9 | unlinkSync(outputFilePath); 10 | } 11 | 12 | const readStream = createReadStream(inputFilePath, { encoding: 'utf-8' }); 13 | const writeStream = createWriteStream(outputFilePath, { encoding: 'utf-8' }); 14 | 15 | let modifiedContent = ''; 16 | 17 | const startString = ''; 18 | const endString = ''; 19 | let skip = false; 20 | 21 | readStream.on('data', chunk => { 22 | const lines = chunk.split('\n'); 23 | for (let line of lines) { 24 | if (line.includes(startString)) { 25 | skip = true; 26 | continue; 27 | } 28 | if (line.includes(endString)) { 29 | skip = false; 30 | continue; 31 | } 32 | if (!skip) modifiedContent += line + '\n'; 33 | } 34 | }); 35 | 36 | readStream.on('close', () => { 37 | writeStream.write(modifiedContent); 38 | writeStream.end(); 39 | }); 40 | 41 | readStream.on('error', err => { 42 | console.error(`Error while reading file: ${inputFilePath}`); 43 | console.error(err); 44 | }); 45 | 46 | writeStream.on('finish', () => { 47 | console.log(`The file ${outputFilePath} has been created!`); 48 | }); 49 | 50 | writeStream.on('error', err => { 51 | console.error(`Error while writing file: ${outputFilePath}`); 52 | console.error(err); 53 | }); 54 | -------------------------------------------------------------------------------- /src/scss/_main.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Josefin Sans', sans-serif; 3 | } 4 | 5 | h1 { 6 | font-weight: 300; 7 | } 8 | 9 | a { 10 | text-decoration: none; 11 | } 12 | 13 | .main-layout { 14 | min-height: 100vh; 15 | min-height: 100dvh; 16 | display: grid; 17 | grid-template-rows: auto 1fr auto auto; 18 | } 19 | 20 | .hidden { 21 | display: none; 22 | } 23 | 24 | .navbar-brand { 25 | font-family: 'Kaushan Script', cursive; 26 | font-size: xx-large; 27 | font-weight: 500; 28 | color: #fd6c3f !important; 29 | letter-spacing: 4px; 30 | } 31 | 32 | .message-text { 33 | overflow: hidden; 34 | resize: none; 35 | // min-height: 45px; 36 | ::-webkit-scrollbar { 37 | width: 0px; 38 | background: transparent; 39 | } 40 | ::-webkit-scrollbar-track { 41 | background: transparent; 42 | } 43 | ::-webkit-scrollbar-thumb { 44 | background: transparent; 45 | } 46 | } 47 | 48 | .api-key-text, 49 | .role-switch, 50 | .role-system, 51 | .message-delete { 52 | user-select: none; 53 | border: var(--bs-border-width) solid var(--bs-border-color) !important; 54 | } 55 | 56 | .api-key-text { 57 | padding-right: 1rem !important; 58 | } 59 | 60 | .message-delete { 61 | font-size: 10px; 62 | width: min-content; 63 | } 64 | 65 | #chatgpt-form div > label { 66 | font-size: medium; 67 | } 68 | 69 | .role-switch { 70 | cursor: pointer; 71 | text-align: center; 72 | } 73 | 74 | .text-small { 75 | font-size: 0.8rem !important; 76 | line-height: 0rem !important; 77 | } 78 | 79 | .input-group .btn { 80 | padding: 6px 3px !important; 81 | border-radius: 0px !important; 82 | } 83 | 84 | #chatgpt-form { 85 | textarea { 86 | padding-top: 0.4rem !important; 87 | border-radius: 0px !important; 88 | outline: none !important; 89 | } 90 | input { 91 | border-radius: 0px !important; 92 | } 93 | } 94 | 95 | .message-delete { 96 | min-width: 1rem !important; 97 | } 98 | 99 | .input-group-text { 100 | border-radius: 0px !important; 101 | } 102 | 103 | pre { 104 | background-color: #282a36; 105 | padding: 1rem; 106 | } 107 | 108 | // main { 109 | // min-height: calc(100vh - 148px) !important; 110 | // } 111 | 112 | #chatgpt-model { 113 | option { 114 | cursor: pointer !important; 115 | padding: 1rem !important; 116 | } 117 | } 118 | 119 | // loading ellipsis animation 120 | 121 | .loading:after { 122 | overflow: hidden; 123 | display: inline-block; 124 | vertical-align: bottom; 125 | -webkit-animation: ellipsis steps(4, end) 900ms infinite; 126 | animation: ellipsis steps(4, end) 900ms infinite; 127 | content: '\2026'; /* ascii code for the ellipsis character */ 128 | width: 0px; 129 | } 130 | 131 | @keyframes ellipsis { 132 | to { 133 | width: 1.25em; 134 | } 135 | } 136 | 137 | @-webkit-keyframes ellipsis { 138 | to { 139 | width: 1.25em; 140 | } 141 | } 142 | 143 | // end of loading ellipsis animation. 144 | 145 | .preview p:last-child { 146 | margin-bottom: 0 !important; 147 | padding-bottom: 0 !important; 148 | } 149 | 150 | .form-control:focus { 151 | box-shadow: none !important; 152 | border-color: #ced4da; 153 | } 154 | -------------------------------------------------------------------------------- /src/scss/_vendor.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/bootstrap.scss'; 2 | @import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@300;400&family=Kaushan+Script&display=swap'); 3 | 4 | table { 5 | @extend .table !optional; 6 | @extend .table-hover !optional; 7 | } 8 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_accordion.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Base styles 3 | // 4 | 5 | .accordion { 6 | // scss-docs-start accordion-css-vars 7 | --#{$prefix}accordion-color: #{$accordion-color}; 8 | --#{$prefix}accordion-bg: #{$accordion-bg}; 9 | --#{$prefix}accordion-transition: #{$accordion-transition}; 10 | --#{$prefix}accordion-border-color: #{$accordion-border-color}; 11 | --#{$prefix}accordion-border-width: #{$accordion-border-width}; 12 | --#{$prefix}accordion-border-radius: #{$accordion-border-radius}; 13 | --#{$prefix}accordion-inner-border-radius: #{$accordion-inner-border-radius}; 14 | --#{$prefix}accordion-btn-padding-x: #{$accordion-button-padding-x}; 15 | --#{$prefix}accordion-btn-padding-y: #{$accordion-button-padding-y}; 16 | --#{$prefix}accordion-btn-color: #{$accordion-button-color}; 17 | --#{$prefix}accordion-btn-bg: #{$accordion-button-bg}; 18 | --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon)}; 19 | --#{$prefix}accordion-btn-icon-width: #{$accordion-icon-width}; 20 | --#{$prefix}accordion-btn-icon-transform: #{$accordion-icon-transform}; 21 | --#{$prefix}accordion-btn-icon-transition: #{$accordion-icon-transition}; 22 | --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon)}; 23 | --#{$prefix}accordion-btn-focus-border-color: #{$accordion-button-focus-border-color}; 24 | --#{$prefix}accordion-btn-focus-box-shadow: #{$accordion-button-focus-box-shadow}; 25 | --#{$prefix}accordion-body-padding-x: #{$accordion-body-padding-x}; 26 | --#{$prefix}accordion-body-padding-y: #{$accordion-body-padding-y}; 27 | --#{$prefix}accordion-active-color: #{$accordion-button-active-color}; 28 | --#{$prefix}accordion-active-bg: #{$accordion-button-active-bg}; 29 | // scss-docs-end accordion-css-vars 30 | } 31 | 32 | .accordion-button { 33 | position: relative; 34 | display: flex; 35 | align-items: center; 36 | width: 100%; 37 | padding: var(--#{$prefix}accordion-btn-padding-y) var(--#{$prefix}accordion-btn-padding-x); 38 | @include font-size($font-size-base); 39 | color: var(--#{$prefix}accordion-btn-color); 40 | text-align: left; // Reset button style 41 | background-color: var(--#{$prefix}accordion-btn-bg); 42 | border: 0; 43 | @include border-radius(0); 44 | overflow-anchor: none; 45 | @include transition(var(--#{$prefix}accordion-transition)); 46 | 47 | &:not(.collapsed) { 48 | color: var(--#{$prefix}accordion-active-color); 49 | background-color: var(--#{$prefix}accordion-active-bg); 50 | box-shadow: inset 0 calc(-1 * var(--#{$prefix}accordion-border-width)) 0 var(--#{$prefix}accordion-border-color); // stylelint-disable-line function-disallowed-list 51 | 52 | &::after { 53 | background-image: var(--#{$prefix}accordion-btn-active-icon); 54 | transform: var(--#{$prefix}accordion-btn-icon-transform); 55 | } 56 | } 57 | 58 | // Accordion icon 59 | &::after { 60 | flex-shrink: 0; 61 | width: var(--#{$prefix}accordion-btn-icon-width); 62 | height: var(--#{$prefix}accordion-btn-icon-width); 63 | margin-left: auto; 64 | content: ""; 65 | background-image: var(--#{$prefix}accordion-btn-icon); 66 | background-repeat: no-repeat; 67 | background-size: var(--#{$prefix}accordion-btn-icon-width); 68 | @include transition(var(--#{$prefix}accordion-btn-icon-transition)); 69 | } 70 | 71 | &:hover { 72 | z-index: 2; 73 | } 74 | 75 | &:focus { 76 | z-index: 3; 77 | border-color: var(--#{$prefix}accordion-btn-focus-border-color); 78 | outline: 0; 79 | box-shadow: var(--#{$prefix}accordion-btn-focus-box-shadow); 80 | } 81 | } 82 | 83 | .accordion-header { 84 | margin-bottom: 0; 85 | } 86 | 87 | .accordion-item { 88 | color: var(--#{$prefix}accordion-color); 89 | background-color: var(--#{$prefix}accordion-bg); 90 | border: var(--#{$prefix}accordion-border-width) solid var(--#{$prefix}accordion-border-color); 91 | 92 | &:first-of-type { 93 | @include border-top-radius(var(--#{$prefix}accordion-border-radius)); 94 | 95 | .accordion-button { 96 | @include border-top-radius(var(--#{$prefix}accordion-inner-border-radius)); 97 | } 98 | } 99 | 100 | &:not(:first-of-type) { 101 | border-top: 0; 102 | } 103 | 104 | // Only set a border-radius on the last item if the accordion is collapsed 105 | &:last-of-type { 106 | @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); 107 | 108 | .accordion-button { 109 | &.collapsed { 110 | @include border-bottom-radius(var(--#{$prefix}accordion-inner-border-radius)); 111 | } 112 | } 113 | 114 | .accordion-collapse { 115 | @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); 116 | } 117 | } 118 | } 119 | 120 | .accordion-body { 121 | padding: var(--#{$prefix}accordion-body-padding-y) var(--#{$prefix}accordion-body-padding-x); 122 | } 123 | 124 | 125 | // Flush accordion items 126 | // 127 | // Remove borders and border-radius to keep accordion items edge-to-edge. 128 | 129 | .accordion-flush { 130 | .accordion-collapse { 131 | border-width: 0; 132 | } 133 | 134 | .accordion-item { 135 | border-right: 0; 136 | border-left: 0; 137 | @include border-radius(0); 138 | 139 | &:first-child { border-top: 0; } 140 | &:last-child { border-bottom: 0; } 141 | 142 | .accordion-button { 143 | &, 144 | &.collapsed { 145 | @include border-radius(0); 146 | } 147 | } 148 | } 149 | } 150 | 151 | @if $enable-dark-mode { 152 | @include color-mode(dark) { 153 | .accordion-button::after { 154 | --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon-dark)}; 155 | --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon-dark)}; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_alert.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Base styles 3 | // 4 | 5 | .alert { 6 | // scss-docs-start alert-css-vars 7 | --#{$prefix}alert-bg: transparent; 8 | --#{$prefix}alert-padding-x: #{$alert-padding-x}; 9 | --#{$prefix}alert-padding-y: #{$alert-padding-y}; 10 | --#{$prefix}alert-margin-bottom: #{$alert-margin-bottom}; 11 | --#{$prefix}alert-color: inherit; 12 | --#{$prefix}alert-border-color: transparent; 13 | --#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color); 14 | --#{$prefix}alert-border-radius: #{$alert-border-radius}; 15 | --#{$prefix}alert-link-color: inherit; 16 | // scss-docs-end alert-css-vars 17 | 18 | position: relative; 19 | padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x); 20 | margin-bottom: var(--#{$prefix}alert-margin-bottom); 21 | color: var(--#{$prefix}alert-color); 22 | background-color: var(--#{$prefix}alert-bg); 23 | border: var(--#{$prefix}alert-border); 24 | @include border-radius(var(--#{$prefix}alert-border-radius)); 25 | } 26 | 27 | // Headings for larger alerts 28 | .alert-heading { 29 | // Specified to prevent conflicts of changing $headings-color 30 | color: inherit; 31 | } 32 | 33 | // Provide class for links that match alerts 34 | .alert-link { 35 | font-weight: $alert-link-font-weight; 36 | color: var(--#{$prefix}alert-link-color); 37 | } 38 | 39 | 40 | // Dismissible alerts 41 | // 42 | // Expand the right padding and account for the close button's positioning. 43 | 44 | .alert-dismissible { 45 | padding-right: $alert-dismissible-padding-r; 46 | 47 | // Adjust close link position 48 | .btn-close { 49 | position: absolute; 50 | top: 0; 51 | right: 0; 52 | z-index: $stretched-link-z-index + 1; 53 | padding: $alert-padding-y * 1.25 $alert-padding-x; 54 | } 55 | } 56 | 57 | 58 | // scss-docs-start alert-modifiers 59 | // Generate contextual modifier classes for colorizing the alert 60 | @each $state in map-keys($theme-colors) { 61 | .alert-#{$state} { 62 | --#{$prefix}alert-color: var(--#{$prefix}#{$state}-text); 63 | --#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle); 64 | --#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle); 65 | --#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text); 66 | } 67 | } 68 | // scss-docs-end alert-modifiers 69 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_badge.scss: -------------------------------------------------------------------------------- 1 | // Base class 2 | // 3 | // Requires one of the contextual, color modifier classes for `color` and 4 | // `background-color`. 5 | 6 | .badge { 7 | // scss-docs-start badge-css-vars 8 | --#{$prefix}badge-padding-x: #{$badge-padding-x}; 9 | --#{$prefix}badge-padding-y: #{$badge-padding-y}; 10 | @include rfs($badge-font-size, --#{$prefix}badge-font-size); 11 | --#{$prefix}badge-font-weight: #{$badge-font-weight}; 12 | --#{$prefix}badge-color: #{$badge-color}; 13 | --#{$prefix}badge-border-radius: #{$badge-border-radius}; 14 | // scss-docs-end badge-css-vars 15 | 16 | display: inline-block; 17 | padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x); 18 | @include font-size(var(--#{$prefix}badge-font-size)); 19 | font-weight: var(--#{$prefix}badge-font-weight); 20 | line-height: 1; 21 | color: var(--#{$prefix}badge-color); 22 | text-align: center; 23 | white-space: nowrap; 24 | vertical-align: baseline; 25 | @include border-radius(var(--#{$prefix}badge-border-radius)); 26 | @include gradient-bg(); 27 | 28 | // Empty badges collapse automatically 29 | &:empty { 30 | display: none; 31 | } 32 | } 33 | 34 | // Quick fix for badges in buttons 35 | .btn .badge { 36 | position: relative; 37 | top: -1px; 38 | } 39 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_breadcrumb.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb { 2 | // scss-docs-start breadcrumb-css-vars 3 | --#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x}; 4 | --#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y}; 5 | --#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom}; 6 | @include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size); 7 | --#{$prefix}breadcrumb-bg: #{$breadcrumb-bg}; 8 | --#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius}; 9 | --#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color}; 10 | --#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x}; 11 | --#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color}; 12 | // scss-docs-end breadcrumb-css-vars 13 | 14 | display: flex; 15 | flex-wrap: wrap; 16 | padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x); 17 | margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom); 18 | @include font-size(var(--#{$prefix}breadcrumb-font-size)); 19 | list-style: none; 20 | background-color: var(--#{$prefix}breadcrumb-bg); 21 | @include border-radius(var(--#{$prefix}breadcrumb-border-radius)); 22 | } 23 | 24 | .breadcrumb-item { 25 | // The separator between breadcrumbs (by default, a forward-slash: "/") 26 | + .breadcrumb-item { 27 | padding-left: var(--#{$prefix}breadcrumb-item-padding-x); 28 | 29 | &::before { 30 | float: left; // Suppress inline spacings and underlining of the separator 31 | padding-right: var(--#{$prefix}breadcrumb-item-padding-x); 32 | color: var(--#{$prefix}breadcrumb-divider-color); 33 | content: var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"}; 34 | } 35 | } 36 | 37 | &.active { 38 | color: var(--#{$prefix}breadcrumb-item-active-color); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_button-group.scss: -------------------------------------------------------------------------------- 1 | // Make the div behave like a button 2 | .btn-group, 3 | .btn-group-vertical { 4 | position: relative; 5 | display: inline-flex; 6 | vertical-align: middle; // match .btn alignment given font-size hack above 7 | 8 | > .btn { 9 | position: relative; 10 | flex: 1 1 auto; 11 | } 12 | 13 | // Bring the hover, focused, and "active" buttons to the front to overlay 14 | // the borders properly 15 | > .btn-check:checked + .btn, 16 | > .btn-check:focus + .btn, 17 | > .btn:hover, 18 | > .btn:focus, 19 | > .btn:active, 20 | > .btn.active { 21 | z-index: 1; 22 | } 23 | } 24 | 25 | // Optional: Group multiple button groups together for a toolbar 26 | .btn-toolbar { 27 | display: flex; 28 | flex-wrap: wrap; 29 | justify-content: flex-start; 30 | 31 | .input-group { 32 | width: auto; 33 | } 34 | } 35 | 36 | .btn-group { 37 | @include border-radius($btn-border-radius); 38 | 39 | // Prevent double borders when buttons are next to each other 40 | > :not(.btn-check:first-child) + .btn, 41 | > .btn-group:not(:first-child) { 42 | margin-left: calc($btn-border-width * -1); // stylelint-disable-line function-disallowed-list 43 | } 44 | 45 | // Reset rounded corners 46 | > .btn:not(:last-child):not(.dropdown-toggle), 47 | > .btn.dropdown-toggle-split:first-child, 48 | > .btn-group:not(:last-child) > .btn { 49 | @include border-end-radius(0); 50 | } 51 | 52 | // The left radius should be 0 if the button is: 53 | // - the "third or more" child 54 | // - the second child and the previous element isn't `.btn-check` (making it the first child visually) 55 | // - part of a btn-group which isn't the first child 56 | > .btn:nth-child(n + 3), 57 | > :not(.btn-check) + .btn, 58 | > .btn-group:not(:first-child) > .btn { 59 | @include border-start-radius(0); 60 | } 61 | } 62 | 63 | // Sizing 64 | // 65 | // Remix the default button sizing classes into new ones for easier manipulation. 66 | 67 | .btn-group-sm > .btn { @extend .btn-sm; } 68 | .btn-group-lg > .btn { @extend .btn-lg; } 69 | 70 | 71 | // 72 | // Split button dropdowns 73 | // 74 | 75 | .dropdown-toggle-split { 76 | padding-right: $btn-padding-x * .75; 77 | padding-left: $btn-padding-x * .75; 78 | 79 | &::after, 80 | .dropup &::after, 81 | .dropend &::after { 82 | margin-left: 0; 83 | } 84 | 85 | .dropstart &::before { 86 | margin-right: 0; 87 | } 88 | } 89 | 90 | .btn-sm + .dropdown-toggle-split { 91 | padding-right: $btn-padding-x-sm * .75; 92 | padding-left: $btn-padding-x-sm * .75; 93 | } 94 | 95 | .btn-lg + .dropdown-toggle-split { 96 | padding-right: $btn-padding-x-lg * .75; 97 | padding-left: $btn-padding-x-lg * .75; 98 | } 99 | 100 | 101 | // The clickable button for toggling the menu 102 | // Set the same inset shadow as the :active state 103 | .btn-group.show .dropdown-toggle { 104 | @include box-shadow($btn-active-box-shadow); 105 | 106 | // Show no shadow for `.btn-link` since it has no other button styles. 107 | &.btn-link { 108 | @include box-shadow(none); 109 | } 110 | } 111 | 112 | 113 | // 114 | // Vertical button groups 115 | // 116 | 117 | .btn-group-vertical { 118 | flex-direction: column; 119 | align-items: flex-start; 120 | justify-content: center; 121 | 122 | > .btn, 123 | > .btn-group { 124 | width: 100%; 125 | } 126 | 127 | > .btn:not(:first-child), 128 | > .btn-group:not(:first-child) { 129 | margin-top: calc($btn-border-width * -1); // stylelint-disable-line function-disallowed-list 130 | } 131 | 132 | // Reset rounded corners 133 | > .btn:not(:last-child):not(.dropdown-toggle), 134 | > .btn-group:not(:last-child) > .btn { 135 | @include border-bottom-radius(0); 136 | } 137 | 138 | > .btn ~ .btn, 139 | > .btn-group:not(:first-child) > .btn { 140 | @include border-top-radius(0); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_buttons.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Base styles 3 | // 4 | 5 | .btn { 6 | // scss-docs-start btn-css-vars 7 | --#{$prefix}btn-padding-x: #{$btn-padding-x}; 8 | --#{$prefix}btn-padding-y: #{$btn-padding-y}; 9 | --#{$prefix}btn-font-family: #{$btn-font-family}; 10 | @include rfs($btn-font-size, --#{$prefix}btn-font-size); 11 | --#{$prefix}btn-font-weight: #{$btn-font-weight}; 12 | --#{$prefix}btn-line-height: #{$btn-line-height}; 13 | --#{$prefix}btn-color: #{$body-color}; 14 | --#{$prefix}btn-bg: transparent; 15 | --#{$prefix}btn-border-width: #{$btn-border-width}; 16 | --#{$prefix}btn-border-color: transparent; 17 | --#{$prefix}btn-border-radius: #{$btn-border-radius}; 18 | --#{$prefix}btn-hover-border-color: transparent; 19 | --#{$prefix}btn-box-shadow: #{$btn-box-shadow}; 20 | --#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity}; 21 | --#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5); 22 | // scss-docs-end btn-css-vars 23 | 24 | display: inline-block; 25 | padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x); 26 | font-family: var(--#{$prefix}btn-font-family); 27 | @include font-size(var(--#{$prefix}btn-font-size)); 28 | font-weight: var(--#{$prefix}btn-font-weight); 29 | line-height: var(--#{$prefix}btn-line-height); 30 | color: var(--#{$prefix}btn-color); 31 | text-align: center; 32 | text-decoration: if($link-decoration == none, null, none); 33 | white-space: $btn-white-space; 34 | vertical-align: middle; 35 | cursor: if($enable-button-pointers, pointer, null); 36 | user-select: none; 37 | border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color); 38 | @include border-radius(var(--#{$prefix}btn-border-radius)); 39 | @include gradient-bg(var(--#{$prefix}btn-bg)); 40 | @include box-shadow(var(--#{$prefix}btn-box-shadow)); 41 | @include transition($btn-transition); 42 | 43 | &:hover { 44 | color: var(--#{$prefix}btn-hover-color); 45 | text-decoration: if($link-hover-decoration == underline, none, null); 46 | background-color: var(--#{$prefix}btn-hover-bg); 47 | border-color: var(--#{$prefix}btn-hover-border-color); 48 | } 49 | 50 | .btn-check + &:hover { 51 | // override for the checkbox/radio buttons 52 | color: var(--#{$prefix}btn-color); 53 | background-color: var(--#{$prefix}btn-bg); 54 | border-color: var(--#{$prefix}btn-border-color); 55 | } 56 | 57 | &:focus-visible { 58 | color: var(--#{$prefix}btn-hover-color); 59 | @include gradient-bg(var(--#{$prefix}btn-hover-bg)); 60 | border-color: var(--#{$prefix}btn-hover-border-color); 61 | outline: 0; 62 | // Avoid using mixin so we can pass custom focus shadow properly 63 | @if $enable-shadows { 64 | box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); 65 | } @else { 66 | box-shadow: var(--#{$prefix}btn-focus-box-shadow); 67 | } 68 | } 69 | 70 | .btn-check:focus-visible + & { 71 | border-color: var(--#{$prefix}btn-hover-border-color); 72 | outline: 0; 73 | // Avoid using mixin so we can pass custom focus shadow properly 74 | @if $enable-shadows { 75 | box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); 76 | } @else { 77 | box-shadow: var(--#{$prefix}btn-focus-box-shadow); 78 | } 79 | } 80 | 81 | .btn-check:checked + &, 82 | :not(.btn-check) + &:active, 83 | &:first-child:active, 84 | &.active, 85 | &.show { 86 | color: var(--#{$prefix}btn-active-color); 87 | background-color: var(--#{$prefix}btn-active-bg); 88 | // Remove CSS gradients if they're enabled 89 | background-image: if($enable-gradients, none, null); 90 | border-color: var(--#{$prefix}btn-active-border-color); 91 | @include box-shadow(var(--#{$prefix}btn-active-shadow)); 92 | 93 | &:focus-visible { 94 | // Avoid using mixin so we can pass custom focus shadow properly 95 | @if $enable-shadows { 96 | box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); 97 | } @else { 98 | box-shadow: var(--#{$prefix}btn-focus-box-shadow); 99 | } 100 | } 101 | } 102 | 103 | &:disabled, 104 | &.disabled, 105 | fieldset:disabled & { 106 | color: var(--#{$prefix}btn-disabled-color); 107 | pointer-events: none; 108 | background-color: var(--#{$prefix}btn-disabled-bg); 109 | background-image: if($enable-gradients, none, null); 110 | border-color: var(--#{$prefix}btn-disabled-border-color); 111 | opacity: var(--#{$prefix}btn-disabled-opacity); 112 | @include box-shadow(none); 113 | } 114 | } 115 | 116 | 117 | // 118 | // Alternate buttons 119 | // 120 | 121 | // scss-docs-start btn-variant-loops 122 | @each $color, $value in $theme-colors { 123 | .btn-#{$color} { 124 | @if $color == "light" { 125 | @include button-variant( 126 | $value, 127 | $value, 128 | $hover-background: shade-color($value, $btn-hover-bg-shade-amount), 129 | $hover-border: shade-color($value, $btn-hover-border-shade-amount), 130 | $active-background: shade-color($value, $btn-active-bg-shade-amount), 131 | $active-border: shade-color($value, $btn-active-border-shade-amount) 132 | ); 133 | } @else if $color == "dark" { 134 | @include button-variant( 135 | $value, 136 | $value, 137 | $hover-background: tint-color($value, $btn-hover-bg-tint-amount), 138 | $hover-border: tint-color($value, $btn-hover-border-tint-amount), 139 | $active-background: tint-color($value, $btn-active-bg-tint-amount), 140 | $active-border: tint-color($value, $btn-active-border-tint-amount) 141 | ); 142 | } @else { 143 | @include button-variant($value, $value); 144 | } 145 | } 146 | } 147 | 148 | @each $color, $value in $theme-colors { 149 | .btn-outline-#{$color} { 150 | @include button-outline-variant($value); 151 | } 152 | } 153 | // scss-docs-end btn-variant-loops 154 | 155 | 156 | // 157 | // Link buttons 158 | // 159 | 160 | // Make a button look and behave like a link 161 | .btn-link { 162 | --#{$prefix}btn-font-weight: #{$font-weight-normal}; 163 | --#{$prefix}btn-color: #{$btn-link-color}; 164 | --#{$prefix}btn-bg: transparent; 165 | --#{$prefix}btn-border-color: transparent; 166 | --#{$prefix}btn-hover-color: #{$btn-link-hover-color}; 167 | --#{$prefix}btn-hover-border-color: transparent; 168 | --#{$prefix}btn-active-color: #{$btn-link-hover-color}; 169 | --#{$prefix}btn-active-border-color: transparent; 170 | --#{$prefix}btn-disabled-color: #{$btn-link-disabled-color}; 171 | --#{$prefix}btn-disabled-border-color: transparent; 172 | --#{$prefix}btn-box-shadow: none; 173 | --#{$prefix}btn-focus-shadow-rgb: #{to-rgb(mix(color-contrast($primary), $primary, 15%))}; 174 | 175 | text-decoration: $link-decoration; 176 | @if $enable-gradients { 177 | background-image: none; 178 | } 179 | 180 | &:hover, 181 | &:focus-visible { 182 | text-decoration: $link-hover-decoration; 183 | } 184 | 185 | &:focus-visible { 186 | color: var(--#{$prefix}btn-color); 187 | } 188 | 189 | &:hover { 190 | color: var(--#{$prefix}btn-hover-color); 191 | } 192 | 193 | // No need for an active state here 194 | } 195 | 196 | 197 | // 198 | // Button Sizes 199 | // 200 | 201 | .btn-lg { 202 | @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); 203 | } 204 | 205 | .btn-sm { 206 | @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); 207 | } 208 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_carousel.scss: -------------------------------------------------------------------------------- 1 | // Notes on the classes: 2 | // 3 | // 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically) 4 | // even when their scroll action started on a carousel, but for compatibility (with Firefox) 5 | // we're preventing all actions instead 6 | // 2. The .carousel-item-start and .carousel-item-end is used to indicate where 7 | // the active slide is heading. 8 | // 3. .active.carousel-item is the current slide. 9 | // 4. .active.carousel-item-start and .active.carousel-item-end is the current 10 | // slide in its in-transition state. Only one of these occurs at a time. 11 | // 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end 12 | // is the upcoming slide in transition. 13 | 14 | .carousel { 15 | position: relative; 16 | } 17 | 18 | .carousel.pointer-event { 19 | touch-action: pan-y; 20 | } 21 | 22 | .carousel-inner { 23 | position: relative; 24 | width: 100%; 25 | overflow: hidden; 26 | @include clearfix(); 27 | } 28 | 29 | .carousel-item { 30 | position: relative; 31 | display: none; 32 | float: left; 33 | width: 100%; 34 | margin-right: -100%; 35 | backface-visibility: hidden; 36 | @include transition($carousel-transition); 37 | } 38 | 39 | .carousel-item.active, 40 | .carousel-item-next, 41 | .carousel-item-prev { 42 | display: block; 43 | } 44 | 45 | .carousel-item-next:not(.carousel-item-start), 46 | .active.carousel-item-end { 47 | transform: translateX(100%); 48 | } 49 | 50 | .carousel-item-prev:not(.carousel-item-end), 51 | .active.carousel-item-start { 52 | transform: translateX(-100%); 53 | } 54 | 55 | 56 | // 57 | // Alternate transitions 58 | // 59 | 60 | .carousel-fade { 61 | .carousel-item { 62 | opacity: 0; 63 | transition-property: opacity; 64 | transform: none; 65 | } 66 | 67 | .carousel-item.active, 68 | .carousel-item-next.carousel-item-start, 69 | .carousel-item-prev.carousel-item-end { 70 | z-index: 1; 71 | opacity: 1; 72 | } 73 | 74 | .active.carousel-item-start, 75 | .active.carousel-item-end { 76 | z-index: 0; 77 | opacity: 0; 78 | @include transition(opacity 0s $carousel-transition-duration); 79 | } 80 | } 81 | 82 | 83 | // 84 | // Left/right controls for nav 85 | // 86 | 87 | .carousel-control-prev, 88 | .carousel-control-next { 89 | position: absolute; 90 | top: 0; 91 | bottom: 0; 92 | z-index: 1; 93 | // Use flex for alignment (1-3) 94 | display: flex; // 1. allow flex styles 95 | align-items: center; // 2. vertically center contents 96 | justify-content: center; // 3. horizontally center contents 97 | width: $carousel-control-width; 98 | padding: 0; 99 | color: $carousel-control-color; 100 | text-align: center; 101 | background: none; 102 | border: 0; 103 | opacity: $carousel-control-opacity; 104 | @include transition($carousel-control-transition); 105 | 106 | // Hover/focus state 107 | &:hover, 108 | &:focus { 109 | color: $carousel-control-color; 110 | text-decoration: none; 111 | outline: 0; 112 | opacity: $carousel-control-hover-opacity; 113 | } 114 | } 115 | .carousel-control-prev { 116 | left: 0; 117 | background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null); 118 | } 119 | .carousel-control-next { 120 | right: 0; 121 | background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null); 122 | } 123 | 124 | // Icons for within 125 | .carousel-control-prev-icon, 126 | .carousel-control-next-icon { 127 | display: inline-block; 128 | width: $carousel-control-icon-width; 129 | height: $carousel-control-icon-width; 130 | background-repeat: no-repeat; 131 | background-position: 50%; 132 | background-size: 100% 100%; 133 | } 134 | 135 | /* rtl:options: { 136 | "autoRename": true, 137 | "stringMap":[ { 138 | "name" : "prev-next", 139 | "search" : "prev", 140 | "replace" : "next" 141 | } ] 142 | } */ 143 | .carousel-control-prev-icon { 144 | background-image: escape-svg($carousel-control-prev-icon-bg); 145 | } 146 | .carousel-control-next-icon { 147 | background-image: escape-svg($carousel-control-next-icon-bg); 148 | } 149 | 150 | // Optional indicator pips/controls 151 | // 152 | // Add a container (such as a list) with the following class and add an item (ideally a focusable control, 153 | // like a button) with data-bs-target for each slide your carousel holds. 154 | 155 | .carousel-indicators { 156 | position: absolute; 157 | right: 0; 158 | bottom: 0; 159 | left: 0; 160 | z-index: 2; 161 | display: flex; 162 | justify-content: center; 163 | padding: 0; 164 | // Use the .carousel-control's width as margin so we don't overlay those 165 | margin-right: $carousel-control-width; 166 | margin-bottom: 1rem; 167 | margin-left: $carousel-control-width; 168 | list-style: none; 169 | 170 | [data-bs-target] { 171 | box-sizing: content-box; 172 | flex: 0 1 auto; 173 | width: $carousel-indicator-width; 174 | height: $carousel-indicator-height; 175 | padding: 0; 176 | margin-right: $carousel-indicator-spacer; 177 | margin-left: $carousel-indicator-spacer; 178 | text-indent: -999px; 179 | cursor: pointer; 180 | background-color: $carousel-indicator-active-bg; 181 | background-clip: padding-box; 182 | border: 0; 183 | // Use transparent borders to increase the hit area by 10px on top and bottom. 184 | border-top: $carousel-indicator-hit-area-height solid transparent; 185 | border-bottom: $carousel-indicator-hit-area-height solid transparent; 186 | opacity: $carousel-indicator-opacity; 187 | @include transition($carousel-indicator-transition); 188 | } 189 | 190 | .active { 191 | opacity: $carousel-indicator-active-opacity; 192 | } 193 | } 194 | 195 | 196 | // Optional captions 197 | // 198 | // 199 | 200 | .carousel-caption { 201 | position: absolute; 202 | right: (100% - $carousel-caption-width) * .5; 203 | bottom: $carousel-caption-spacer; 204 | left: (100% - $carousel-caption-width) * .5; 205 | padding-top: $carousel-caption-padding-y; 206 | padding-bottom: $carousel-caption-padding-y; 207 | color: $carousel-caption-color; 208 | text-align: center; 209 | } 210 | 211 | // Dark mode carousel 212 | 213 | @mixin carousel-dark() { 214 | .carousel-control-prev-icon, 215 | .carousel-control-next-icon { 216 | filter: $carousel-dark-control-icon-filter; 217 | } 218 | 219 | .carousel-indicators [data-bs-target] { 220 | background-color: $carousel-dark-indicator-active-bg; 221 | } 222 | 223 | .carousel-caption { 224 | color: $carousel-dark-caption-color; 225 | } 226 | } 227 | 228 | .carousel-dark { 229 | @include carousel-dark(); 230 | } 231 | 232 | @if $enable-dark-mode { 233 | @include color-mode(dark) { 234 | .carousel { 235 | @include carousel-dark(); 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_close.scss: -------------------------------------------------------------------------------- 1 | // Transparent background and border properties included for button version. 2 | // iOS requires the button element instead of an anchor tag. 3 | // If you want the anchor version, it requires `href="#"`. 4 | // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile 5 | 6 | .btn-close { 7 | --#{$prefix}btn-close-color: #{$btn-close-color}; 8 | --#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg) }; 9 | --#{$prefix}btn-close-opacity: #{$btn-close-opacity}; 10 | --#{$prefix}btn-close-hover-opacity: #{$btn-close-hover-opacity}; 11 | --#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow}; 12 | --#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity}; 13 | --#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity}; 14 | --#{$prefix}btn-close-white-filter: #{$btn-close-white-filter}; 15 | 16 | box-sizing: content-box; 17 | width: $btn-close-width; 18 | height: $btn-close-height; 19 | padding: $btn-close-padding-y $btn-close-padding-x; 20 | color: var(--#{$prefix}btn-close-color); 21 | background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements 22 | border: 0; // for button elements 23 | @include border-radius(); 24 | opacity: var(--#{$prefix}btn-close-opacity); 25 | 26 | // Override 's hover style 27 | &:hover { 28 | color: var(--#{$prefix}btn-close-color); 29 | text-decoration: none; 30 | opacity: var(--#{$prefix}btn-close-hover-opacity); 31 | } 32 | 33 | &:focus { 34 | outline: 0; 35 | box-shadow: var(--#{$prefix}btn-close-focus-shadow); 36 | opacity: var(--#{$prefix}btn-close-focus-opacity); 37 | } 38 | 39 | &:disabled, 40 | &.disabled { 41 | pointer-events: none; 42 | user-select: none; 43 | opacity: var(--#{$prefix}btn-close-disabled-opacity); 44 | } 45 | } 46 | 47 | @mixin btn-close-white() { 48 | filter: var(--#{$prefix}btn-close-white-filter); 49 | } 50 | 51 | .btn-close-white { 52 | @include btn-close-white(); 53 | } 54 | 55 | @if $enable-dark-mode { 56 | @include color-mode(dark) { 57 | .btn-close { 58 | @include btn-close-white(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_containers.scss: -------------------------------------------------------------------------------- 1 | // Container widths 2 | // 3 | // Set the container width, and override it for fixed navbars in media queries. 4 | 5 | @if $enable-container-classes { 6 | // Single container class with breakpoint max-widths 7 | .container, 8 | // 100% wide container at all breakpoints 9 | .container-fluid { 10 | @include make-container(); 11 | } 12 | 13 | // Responsive containers that are 100% wide until a breakpoint 14 | @each $breakpoint, $container-max-width in $container-max-widths { 15 | .container-#{$breakpoint} { 16 | @extend .container-fluid; 17 | } 18 | 19 | @include media-breakpoint-up($breakpoint, $grid-breakpoints) { 20 | %responsive-container-#{$breakpoint} { 21 | max-width: $container-max-width; 22 | } 23 | 24 | // Extend each breakpoint which is smaller or equal to the current breakpoint 25 | $extend-breakpoint: true; 26 | 27 | @each $name, $width in $grid-breakpoints { 28 | @if ($extend-breakpoint) { 29 | .container#{breakpoint-infix($name, $grid-breakpoints)} { 30 | @extend %responsive-container-#{$breakpoint}; 31 | } 32 | 33 | // Once the current breakpoint is reached, stop extending 34 | @if ($breakpoint == $name) { 35 | $extend-breakpoint: false; 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_forms.scss: -------------------------------------------------------------------------------- 1 | @import "forms/labels"; 2 | @import "forms/form-text"; 3 | @import "forms/form-control"; 4 | @import "forms/form-select"; 5 | @import "forms/form-check"; 6 | @import "forms/form-range"; 7 | @import "forms/floating-labels"; 8 | @import "forms/input-group"; 9 | @import "forms/validation"; 10 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_grid.scss: -------------------------------------------------------------------------------- 1 | // Row 2 | // 3 | // Rows contain your columns. 4 | 5 | @if $enable-grid-classes { 6 | .row { 7 | @include make-row(); 8 | 9 | > * { 10 | @include make-col-ready(); 11 | } 12 | } 13 | } 14 | 15 | @if $enable-cssgrid { 16 | .grid { 17 | display: grid; 18 | grid-template-rows: repeat(var(--#{$prefix}rows, 1), 1fr); 19 | grid-template-columns: repeat(var(--#{$prefix}columns, #{$grid-columns}), 1fr); 20 | gap: var(--#{$prefix}gap, #{$grid-gutter-width}); 21 | 22 | @include make-cssgrid(); 23 | } 24 | } 25 | 26 | 27 | // Columns 28 | // 29 | // Common styles for small and large grid columns 30 | 31 | @if $enable-grid-classes { 32 | @include make-grid-columns(); 33 | } 34 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_helpers.scss: -------------------------------------------------------------------------------- 1 | @import "helpers/clearfix"; 2 | @import "helpers/color-bg"; 3 | @import "helpers/colored-links"; 4 | @import "helpers/ratio"; 5 | @import "helpers/position"; 6 | @import "helpers/stacks"; 7 | @import "helpers/visually-hidden"; 8 | @import "helpers/stretched-link"; 9 | @import "helpers/text-truncation"; 10 | @import "helpers/vr"; 11 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_images.scss: -------------------------------------------------------------------------------- 1 | // Responsive images (ensure images don't scale beyond their parents) 2 | // 3 | // This is purposefully opt-in via an explicit class rather than being the default for all ``s. 4 | // We previously tried the "images are responsive by default" approach in Bootstrap v2, 5 | // and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps) 6 | // which weren't expecting the images within themselves to be involuntarily resized. 7 | // See also https://github.com/twbs/bootstrap/issues/18178 8 | .img-fluid { 9 | @include img-fluid(); 10 | } 11 | 12 | 13 | // Image thumbnails 14 | .img-thumbnail { 15 | padding: $thumbnail-padding; 16 | background-color: $thumbnail-bg; 17 | border: $thumbnail-border-width solid $thumbnail-border-color; 18 | @include border-radius($thumbnail-border-radius); 19 | @include box-shadow($thumbnail-box-shadow); 20 | 21 | // Keep them at most 100% wide 22 | @include img-fluid(); 23 | } 24 | 25 | // 26 | // Figures 27 | // 28 | 29 | .figure { 30 | // Ensures the caption's text aligns with the image. 31 | display: inline-block; 32 | } 33 | 34 | .figure-img { 35 | margin-bottom: $spacer * .5; 36 | line-height: 1; 37 | } 38 | 39 | .figure-caption { 40 | @include font-size($figure-caption-font-size); 41 | color: $figure-caption-color; 42 | } 43 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_maps.scss: -------------------------------------------------------------------------------- 1 | // Re-assigned maps 2 | // 3 | // Placed here so that others can override the default Sass maps and see automatic updates to utilities and more. 4 | 5 | // scss-docs-start theme-colors-rgb 6 | $theme-colors-rgb: map-loop($theme-colors, to-rgb, "$value") !default; 7 | // scss-docs-end theme-colors-rgb 8 | 9 | $theme-colors-text: ( 10 | "primary": $primary-text, 11 | "secondary": $secondary-text, 12 | "success": $success-text, 13 | "info": $info-text, 14 | "warning": $warning-text, 15 | "danger": $danger-text, 16 | "light": $light-text, 17 | "dark": $dark-text, 18 | ) !default; 19 | 20 | $theme-colors-bg-subtle: ( 21 | "primary": $primary-bg-subtle, 22 | "secondary": $secondary-bg-subtle, 23 | "success": $success-bg-subtle, 24 | "info": $info-bg-subtle, 25 | "warning": $warning-bg-subtle, 26 | "danger": $danger-bg-subtle, 27 | "light": $light-bg-subtle, 28 | "dark": $dark-bg-subtle, 29 | ) !default; 30 | 31 | $theme-colors-border-subtle: ( 32 | "primary": $primary-border-subtle, 33 | "secondary": $secondary-border-subtle, 34 | "success": $success-border-subtle, 35 | "info": $info-border-subtle, 36 | "warning": $warning-border-subtle, 37 | "danger": $danger-border-subtle, 38 | "light": $light-border-subtle, 39 | "dark": $dark-border-subtle, 40 | ) !default; 41 | 42 | // Utilities maps 43 | // 44 | // Extends the default `$theme-colors` maps to help create our utilities. 45 | 46 | // Come v6, we'll de-dupe these variables. Until then, for backward compatibility, we keep them to reassign. 47 | // scss-docs-start utilities-colors 48 | $utilities-colors: $theme-colors-rgb !default; 49 | // scss-docs-end utilities-colors 50 | 51 | // scss-docs-start utilities-text-colors 52 | $utilities-text: map-merge( 53 | $utilities-colors, 54 | ( 55 | "black": to-rgb($black), 56 | "white": to-rgb($white), 57 | "body": to-rgb($body-color) 58 | ) 59 | ) !default; 60 | $utilities-text-colors: map-loop($utilities-text, rgba-css-var, "$key", "text") !default; 61 | 62 | $utilities-text-emphasis-colors: ( 63 | "primary-emphasis": var(--#{$prefix}primary-text), 64 | "secondary-emphasis": var(--#{$prefix}secondary-text), 65 | "success-emphasis": var(--#{$prefix}success-text), 66 | "info-emphasis": var(--#{$prefix}info-text), 67 | "warning-emphasis": var(--#{$prefix}warning-text), 68 | "danger-emphasis": var(--#{$prefix}danger-text), 69 | "light-emphasis": var(--#{$prefix}light-text), 70 | "dark-emphasis": var(--#{$prefix}dark-text) 71 | ) !default; 72 | // scss-docs-end utilities-text-colors 73 | 74 | // scss-docs-start utilities-bg-colors 75 | $utilities-bg: map-merge( 76 | $utilities-colors, 77 | ( 78 | "black": to-rgb($black), 79 | "white": to-rgb($white), 80 | "body": to-rgb($body-bg) 81 | ) 82 | ) !default; 83 | $utilities-bg-colors: map-loop($utilities-bg, rgba-css-var, "$key", "bg") !default; 84 | 85 | $utilities-bg-subtle: ( 86 | "primary-subtle": var(--#{$prefix}primary-bg-subtle), 87 | "secondary-subtle": var(--#{$prefix}secondary-bg-subtle), 88 | "success-subtle": var(--#{$prefix}success-bg-subtle), 89 | "info-subtle": var(--#{$prefix}info-bg-subtle), 90 | "warning-subtle": var(--#{$prefix}warning-bg-subtle), 91 | "danger-subtle": var(--#{$prefix}danger-bg-subtle), 92 | "light-subtle": var(--#{$prefix}light-bg-subtle), 93 | "dark-subtle": var(--#{$prefix}dark-bg-subtle) 94 | ) !default; 95 | // $utilities-bg-subtle-colors: map-loop($utilities-bg-subtle, rgba-css-var, "$key", "bg") !default; 96 | // scss-docs-end utilities-bg-colors 97 | 98 | // scss-docs-start utilities-border-colors 99 | $utilities-border: map-merge( 100 | $utilities-colors, 101 | ( 102 | "white": to-rgb($white) 103 | ) 104 | ) !default; 105 | $utilities-border-colors: map-loop($utilities-border, rgba-css-var, "$key", "border") !default; 106 | 107 | $utilities-border-subtle: ( 108 | "primary-subtle": var(--#{$prefix}primary-border-subtle), 109 | "secondary-subtle": var(--#{$prefix}secondary-border-subtle), 110 | "success-subtle": var(--#{$prefix}success-border-subtle), 111 | "info-subtle": var(--#{$prefix}info-border-subtle), 112 | "warning-subtle": var(--#{$prefix}warning-border-subtle), 113 | "danger-subtle": var(--#{$prefix}danger-border-subtle), 114 | "light-subtle": var(--#{$prefix}light-border-subtle), 115 | "dark-subtle": var(--#{$prefix}dark-border-subtle) 116 | ) !default; 117 | // scss-docs-end utilities-border-colors 118 | 119 | $negative-spacers: if($enable-negative-margins, negativify-map($spacers), null) !default; 120 | 121 | $gutters: $spacers !default; 122 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Toggles 2 | // 3 | // Used in conjunction with global variables to enable certain theme features. 4 | 5 | // Vendor 6 | @import "vendor/rfs"; 7 | 8 | // Deprecate 9 | @import "mixins/deprecate"; 10 | 11 | // Helpers 12 | @import "mixins/breakpoints"; 13 | @import "mixins/color-mode"; 14 | @import "mixins/color-scheme"; 15 | @import "mixins/image"; 16 | @import "mixins/resize"; 17 | @import "mixins/visually-hidden"; 18 | @import "mixins/reset-text"; 19 | @import "mixins/text-truncate"; 20 | 21 | // Utilities 22 | @import "mixins/utilities"; 23 | 24 | // Components 25 | @import "mixins/backdrop"; 26 | @import "mixins/buttons"; 27 | @import "mixins/caret"; 28 | @import "mixins/pagination"; 29 | @import "mixins/lists"; 30 | @import "mixins/forms"; 31 | @import "mixins/table-variants"; 32 | 33 | // Skins 34 | @import "mixins/border-radius"; 35 | @import "mixins/box-shadow"; 36 | @import "mixins/gradients"; 37 | @import "mixins/transition"; 38 | 39 | // Layout 40 | @import "mixins/clearfix"; 41 | @import "mixins/container"; 42 | @import "mixins/grid"; 43 | -------------------------------------------------------------------------------- /src/scss/bootstrap/_nav.scss: -------------------------------------------------------------------------------- 1 | // Base class 2 | // 3 | // Kickstart any navigation component with a set of style resets. Works with 4 | // `