├── .github ├── assets │ └── hero.png └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── license.txt ├── package.json ├── pyproject.toml ├── tldraw_whiteboard ├── __init__.py ├── api.py ├── config │ └── __init__.py ├── hooks.py ├── modules.txt ├── patches.txt ├── public │ ├── .gitkeep │ ├── css │ │ └── whiteboard.bundle.css │ └── js │ │ ├── whiteboard │ │ ├── App.jsx │ │ ├── DBStateManager.jsx │ │ └── whiteboard.bundle.jsx │ │ └── whiteboard_custom.js ├── templates │ ├── __init__.py │ └── pages │ │ └── __init__.py └── tldraw_whiteboard │ ├── __init__.py │ ├── doctype │ ├── __init__.py │ └── user_whiteboard_state │ │ ├── __init__.py │ │ ├── test_user_whiteboard_state.py │ │ ├── user_whiteboard_state.js │ │ ├── user_whiteboard_state.json │ │ └── user_whiteboard_state.py │ └── page │ ├── __init__.py │ └── whiteboard │ ├── __init__.py │ ├── whiteboard.js │ └── whiteboard.json └── yarn.lock /.github/assets/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NagariaHussain/tldraw_whiteboard/c18a52e456a6218b744ee3f3a2f03e7062139935/.github/assets/hero.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - develop 8 | pull_request: 9 | 10 | concurrency: 11 | group: develop-tldraw_whiteboard-${{ github.event.number }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | tests: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | name: Server 20 | 21 | services: 22 | redis-cache: 23 | image: redis:alpine 24 | ports: 25 | - 13000:6379 26 | redis-queue: 27 | image: redis:alpine 28 | ports: 29 | - 11000:6379 30 | mariadb: 31 | image: mariadb:10.6 32 | env: 33 | MYSQL_ROOT_PASSWORD: root 34 | ports: 35 | - 3306:3306 36 | options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 37 | 38 | steps: 39 | - name: Clone 40 | uses: actions/checkout@v3 41 | 42 | - name: Setup Python 43 | uses: actions/setup-python@v4 44 | with: 45 | python-version: '3.10' 46 | 47 | - name: Setup Node 48 | uses: actions/setup-node@v3 49 | with: 50 | node-version: 18 51 | check-latest: true 52 | 53 | - name: Cache pip 54 | uses: actions/cache@v2 55 | with: 56 | path: ~/.cache/pip 57 | key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} 58 | restore-keys: | 59 | ${{ runner.os }}-pip- 60 | ${{ runner.os }}- 61 | 62 | - name: Get yarn cache directory path 63 | id: yarn-cache-dir-path 64 | run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT' 65 | 66 | - uses: actions/cache@v3 67 | id: yarn-cache 68 | with: 69 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 70 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 71 | restore-keys: | 72 | ${{ runner.os }}-yarn- 73 | 74 | - name: Install MariaDB Client 75 | run: sudo apt-get install mariadb-client-10.6 76 | 77 | - name: Setup 78 | run: | 79 | pip install frappe-bench 80 | bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench 81 | mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" 82 | mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" 83 | 84 | - name: Install 85 | working-directory: /home/runner/frappe-bench 86 | run: | 87 | bench get-app tldraw_whiteboard $GITHUB_WORKSPACE 88 | bench setup requirements --dev 89 | bench new-site --db-root-password root --admin-password admin test_site 90 | bench --site test_site install-app tldraw_whiteboard 91 | bench build 92 | env: 93 | CI: 'Yes' 94 | 95 | - name: Run Tests 96 | working-directory: /home/runner/frappe-bench 97 | run: | 98 | bench --site test_site set-config allow_tests true 99 | bench --site test_site run-tests --app tldraw_whiteboard 100 | env: 101 | TYPE: server 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | node_modules 7 | __pycache__ 8 | tldraw_whiteboard/public/dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Tldraw Whiteboard 2 | 3 | Feature rich whiteboard, powered by tldraw.dev, right in your Frappe desk. 4 | 5 |  6 | 7 | ### Keyboard Shortcut 8 | 9 | You can press Shift + Ctrl + O to open the whiteboard from anywhere! 10 | 11 | #### License 12 | 13 | mit 14 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tldraw_whiteboard", 3 | "version": "1.0.0", 4 | "description": "Whiteboard, right in desk, feature reach.", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@tldraw/tldraw": "^2.0.0-alpha.18", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tldraw_whiteboard" 3 | authors = [ 4 | { name = "Build With Hussain", email = "hussain@frappe.io"} 5 | ] 6 | description = "Whiteboard, right in desk, feature reach." 7 | requires-python = ">=3.10" 8 | readme = "README.md" 9 | dynamic = ["version"] 10 | dependencies = [ 11 | # "frappe~=15.0.0" # Installed and managed by bench. 12 | ] 13 | 14 | [build-system] 15 | requires = ["flit_core >=3.4,<4"] 16 | build-backend = "flit_core.buildapi" 17 | 18 | # These dependencies are only installed when developer mode is enabled 19 | [tool.bench.dev-dependencies] 20 | # package_name = "~=1.1.0" 21 | -------------------------------------------------------------------------------- /tldraw_whiteboard/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '0.0.1' 3 | 4 | -------------------------------------------------------------------------------- /tldraw_whiteboard/api.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | @frappe.whitelist() 5 | def save_user_whiteboard_state(state: str): 6 | """Save the logged in user's whiteboard state to the database.""" 7 | exists = frappe.db.exists("User Whiteboard State", frappe.session.user) 8 | 9 | if not exists: 10 | doc = frappe.new_doc("User Whiteboard State") 11 | doc.user = frappe.session.user 12 | doc.state = state 13 | doc.insert(ignore_permissions=True) 14 | else: 15 | frappe.db.set_value( 16 | "User Whiteboard State", frappe.session.user, "state", state 17 | ) 18 | -------------------------------------------------------------------------------- /tldraw_whiteboard/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NagariaHussain/tldraw_whiteboard/c18a52e456a6218b744ee3f3a2f03e7062139935/tldraw_whiteboard/config/__init__.py -------------------------------------------------------------------------------- /tldraw_whiteboard/hooks.py: -------------------------------------------------------------------------------- 1 | app_name = "tldraw_whiteboard" 2 | app_title = "Tldraw Whiteboard" 3 | app_publisher = "Build With Hussain" 4 | app_description = "Whiteboard, right in desk, feature reach." 5 | app_email = "hussain@frappe.io" 6 | app_license = "mit" 7 | # required_apps = [] 8 | 9 | # Includes in
10 | # ------------------ 11 | 12 | # include js, css files in header of desk.html 13 | # app_include_css = "/assets/tldraw_whiteboard/css/tldraw_whiteboard.css" 14 | app_include_js = "/assets/tldraw_whiteboard/js/whiteboard_custom.js" 15 | 16 | # include js, css files in header of web template 17 | # web_include_css = "/assets/tldraw_whiteboard/css/tldraw_whiteboard.css" 18 | # web_include_js = "/assets/tldraw_whiteboard/js/tldraw_whiteboard.js" 19 | 20 | # include custom scss in every website theme (without file extension ".scss") 21 | # website_theme_scss = "tldraw_whiteboard/public/scss/website" 22 | 23 | # include js, css files in header of web form 24 | # webform_include_js = {"doctype": "public/js/doctype.js"} 25 | # webform_include_css = {"doctype": "public/css/doctype.css"} 26 | 27 | # include js in page 28 | # page_js = {"page" : "public/js/file.js"} 29 | 30 | # include js in doctype views 31 | # doctype_js = {"doctype" : "public/js/doctype.js"} 32 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} 33 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} 34 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} 35 | 36 | # Svg Icons 37 | # ------------------ 38 | # include app icons in desk 39 | # app_include_icons = "tldraw_whiteboard/public/icons.svg" 40 | 41 | # Home Pages 42 | # ---------- 43 | 44 | # application home page (will override Website Settings) 45 | # home_page = "login" 46 | 47 | # website user home page (by Role) 48 | # role_home_page = { 49 | # "Role": "home_page" 50 | # } 51 | 52 | # Generators 53 | # ---------- 54 | 55 | # automatically create page for each record of this doctype 56 | # website_generators = ["Web Page"] 57 | 58 | # Jinja 59 | # ---------- 60 | 61 | # add methods and filters to jinja environment 62 | # jinja = { 63 | # "methods": "tldraw_whiteboard.utils.jinja_methods", 64 | # "filters": "tldraw_whiteboard.utils.jinja_filters" 65 | # } 66 | 67 | # Installation 68 | # ------------ 69 | 70 | # before_install = "tldraw_whiteboard.install.before_install" 71 | # after_install = "tldraw_whiteboard.install.after_install" 72 | 73 | # Uninstallation 74 | # ------------ 75 | 76 | # before_uninstall = "tldraw_whiteboard.uninstall.before_uninstall" 77 | # after_uninstall = "tldraw_whiteboard.uninstall.after_uninstall" 78 | 79 | # Integration Setup 80 | # ------------------ 81 | # To set up dependencies/integrations with other apps 82 | # Name of the app being installed is passed as an argument 83 | 84 | # before_app_install = "tldraw_whiteboard.utils.before_app_install" 85 | # after_app_install = "tldraw_whiteboard.utils.after_app_install" 86 | 87 | # Integration Cleanup 88 | # ------------------- 89 | # To clean up dependencies/integrations with other apps 90 | # Name of the app being uninstalled is passed as an argument 91 | 92 | # before_app_uninstall = "tldraw_whiteboard.utils.before_app_uninstall" 93 | # after_app_uninstall = "tldraw_whiteboard.utils.after_app_uninstall" 94 | 95 | # Desk Notifications 96 | # ------------------ 97 | # See frappe.core.notifications.get_notification_config 98 | 99 | # notification_config = "tldraw_whiteboard.notifications.get_notification_config" 100 | 101 | # Permissions 102 | # ----------- 103 | # Permissions evaluated in scripted ways 104 | 105 | # permission_query_conditions = { 106 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", 107 | # } 108 | # 109 | # has_permission = { 110 | # "Event": "frappe.desk.doctype.event.event.has_permission", 111 | # } 112 | 113 | # DocType Class 114 | # --------------- 115 | # Override standard doctype classes 116 | 117 | # override_doctype_class = { 118 | # "ToDo": "custom_app.overrides.CustomToDo" 119 | # } 120 | 121 | # Document Events 122 | # --------------- 123 | # Hook on document methods and events 124 | 125 | # doc_events = { 126 | # "*": { 127 | # "on_update": "method", 128 | # "on_cancel": "method", 129 | # "on_trash": "method" 130 | # } 131 | # } 132 | 133 | # Scheduled Tasks 134 | # --------------- 135 | 136 | # scheduler_events = { 137 | # "all": [ 138 | # "tldraw_whiteboard.tasks.all" 139 | # ], 140 | # "daily": [ 141 | # "tldraw_whiteboard.tasks.daily" 142 | # ], 143 | # "hourly": [ 144 | # "tldraw_whiteboard.tasks.hourly" 145 | # ], 146 | # "weekly": [ 147 | # "tldraw_whiteboard.tasks.weekly" 148 | # ], 149 | # "monthly": [ 150 | # "tldraw_whiteboard.tasks.monthly" 151 | # ], 152 | # } 153 | 154 | # Testing 155 | # ------- 156 | 157 | # before_tests = "tldraw_whiteboard.install.before_tests" 158 | 159 | # Overriding Methods 160 | # ------------------------------ 161 | # 162 | # override_whitelisted_methods = { 163 | # "frappe.desk.doctype.event.event.get_events": "tldraw_whiteboard.event.get_events" 164 | # } 165 | # 166 | # each overriding function accepts a `data` argument; 167 | # generated from the base implementation of the doctype dashboard, 168 | # along with any modifications made in other Frappe apps 169 | # override_doctype_dashboards = { 170 | # "Task": "tldraw_whiteboard.task.get_dashboard_data" 171 | # } 172 | 173 | # exempt linked doctypes from being automatically cancelled 174 | # 175 | # auto_cancel_exempted_doctypes = ["Auto Repeat"] 176 | 177 | # Ignore links to specified DocTypes when deleting documents 178 | # ----------------------------------------------------------- 179 | 180 | # ignore_links_on_delete = ["Communication", "ToDo"] 181 | 182 | # Request Events 183 | # ---------------- 184 | # before_request = ["tldraw_whiteboard.utils.before_request"] 185 | # after_request = ["tldraw_whiteboard.utils.after_request"] 186 | 187 | # Job Events 188 | # ---------- 189 | # before_job = ["tldraw_whiteboard.utils.before_job"] 190 | # after_job = ["tldraw_whiteboard.utils.after_job"] 191 | 192 | # User Data Protection 193 | # -------------------- 194 | 195 | # user_data_fields = [ 196 | # { 197 | # "doctype": "{doctype_1}", 198 | # "filter_by": "{filter_by}", 199 | # "redact_fields": ["{field_1}", "{field_2}"], 200 | # "partial": 1, 201 | # }, 202 | # { 203 | # "doctype": "{doctype_2}", 204 | # "filter_by": "{filter_by}", 205 | # "partial": 1, 206 | # }, 207 | # { 208 | # "doctype": "{doctype_3}", 209 | # "strict": False, 210 | # }, 211 | # { 212 | # "doctype": "{doctype_4}" 213 | # } 214 | # ] 215 | 216 | # Authentication and authorization 217 | # -------------------------------- 218 | 219 | # auth_hooks = [ 220 | # "tldraw_whiteboard.auth.validate" 221 | # ] 222 | -------------------------------------------------------------------------------- /tldraw_whiteboard/modules.txt: -------------------------------------------------------------------------------- 1 | Tldraw Whiteboard -------------------------------------------------------------------------------- /tldraw_whiteboard/patches.txt: -------------------------------------------------------------------------------- 1 | [pre_model_sync] 2 | # Patches added in this section will be executed before doctypes are migrated 3 | # Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations 4 | 5 | [post_model_sync] 6 | # Patches added in this section will be executed after doctypes are migrated -------------------------------------------------------------------------------- /tldraw_whiteboard/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NagariaHussain/tldraw_whiteboard/c18a52e456a6218b744ee3f3a2f03e7062139935/tldraw_whiteboard/public/.gitkeep -------------------------------------------------------------------------------- /tldraw_whiteboard/public/css/whiteboard.bundle.css: -------------------------------------------------------------------------------- 1 | /* THIS CSS FILE IS GENERATED! DO NOT EDIT. OR EDIT. I'M A COMMENT NOT A COP */ 2 | /* This file is created by the copy-css-files.mjs script in @tldraw/tldraw. */ 3 | /* It combines @tldraw/editor's editor.css and @tldraw/tldraw's ui.css */ 4 | 5 | /* @tldraw/editor */ 6 | 7 | .tl-container { 8 | width: 100%; 9 | height: 100%; 10 | font-size: 12px; 11 | /* Spacing */ 12 | --space-1: 2px; 13 | --space-2: 4px; 14 | --space-3: 8px; 15 | --space-4: 12px; 16 | --space-5: 16px; 17 | --space-6: 20px; 18 | --space-7: 28px; 19 | --space-8: 32px; 20 | --space-9: 64px; 21 | --space-10: 72px; 22 | /* Radius */ 23 | --radius-0: 2px; 24 | --radius-1: 4px; 25 | --radius-2: 6px; 26 | --radius-3: 9px; 27 | --radius-4: 13px; 28 | --layer-background: 100; 29 | --layer-grid: 150; 30 | --layer-canvas: 200; 31 | --layer-shapes: 300; 32 | --layer-overlays: 400; 33 | --layer-following-indicator: 1000; 34 | /* Misc */ 35 | --tl-zoom: 1; 36 | --tl-dpr-multiple: 100; 37 | --tl-dpr-multiple-px: calc(var(--tl-dpr-multiple) * 1px); 38 | 39 | /* Cursor SVGs */ 40 | --tl-cursor-none: none; 41 | --tl-cursor-default: url("data:image/svg+xml,") 42 | 12 8, 43 | default; 44 | --tl-cursor-pointer: url("data:image/svg+xml,") 45 | 14 10, 46 | pointer; 47 | --tl-cursor-cross: url("data:image/svg+xml,") 48 | 16 16, 49 | crosshair; 50 | --tl-cursor-move: url("data:image/svg+xml,") 51 | 16 16, 52 | move; 53 | --tl-cursor-grab: url("data:image/svg+xml,") 54 | 16 16, 55 | grab; 56 | --tl-cursor-grabbing: url("data:image/svg+xml,") 57 | 16 16, 58 | grabbing; 59 | --tl-cursor-text: url("data:image/svg+xml,") 60 | 4 10, 61 | text; 62 | --tl-cursor-zoom-in: url("data:image/svg+xml,") 63 | 16 16, 64 | zoom-in; 65 | --tl-cursor-zoom-out: url("data:image/svg+xml,") 66 | 16 16, 67 | zoom-out; 68 | 69 | /* These cursor values get programmatically overridden */ 70 | /* They're just here to help your editor autocomplete */ 71 | --tl-cursor: var(--tl-cursor-default); 72 | --tl-cursor-resize-edge: ew-resize; 73 | --tl-cursor-resize-corner: nesw-resize; 74 | --tl-cursor-ew-resize: ew-resize; 75 | --tl-cursor-ns-resize: ns-resize; 76 | --tl-cursor-nesw-resize: nesw-resize; 77 | --tl-cursor-nwse-resize: nwse-resize; 78 | --tl-cursor-rotate: pointer; 79 | --tl-cursor-nwse-rotate: pointer; 80 | --tl-cursor-nesw-rotate: pointer; 81 | --tl-cursor-senw-rotate: pointer; 82 | --tl-cursor-swne-rotate: pointer; 83 | --tl-scale: calc(1 / var(--tl-zoom)); 84 | --tl-font-draw: 'tldraw_draw', sans-serif; 85 | --tl-font-sans: 'tldraw_sans', sans-serif; 86 | --tl-font-serif: 'tldraw_serif', serif; 87 | --tl-font-mono: 'tldraw_mono', monospace; 88 | --a: calc(min(0.5, 1 / var(--tl-zoom)) * 2px); 89 | --b: calc(min(0.5, 1 / var(--tl-zoom)) * -2px); 90 | --tl-text-outline: 0 var(--b) 0 var(--color-background), 0 var(--a) 0 var(--color-background), 91 | var(--b) var(--b) 0 var(--color-background), var(--a) var(--b) 0 var(--color-background), 92 | var(--a) var(--a) 0 var(--color-background), var(--b) var(--a) 0 var(--color-background); 93 | /* Own properties */ 94 | position: relative; 95 | top: 0px; 96 | left: 0px; 97 | width: 100%; 98 | height: 100%; 99 | overflow: clip; 100 | } 101 | 102 | .tl-theme__light { 103 | --color-accent: #e64a4a; 104 | --color-background: rgb(249, 250, 251); 105 | --color-brush-fill: rgba(144, 144, 144, 0.102); 106 | --color-brush-stroke: rgba(144, 144, 144, 0.251); 107 | --color-grid: rgb(109, 109, 109); 108 | --color-low: hsl(204, 16%, 94%); 109 | --color-low-border: hsl(204, 16%, 92%); 110 | --color-culled: rgb(235, 238, 240); 111 | --color-muted-none: rgba(0, 0, 0, 0); 112 | --color-muted-0: rgba(0, 0, 0, 0.02); 113 | --color-muted-1: rgba(0, 0, 0, 0.1); 114 | --color-muted-2: rgba(0, 0, 0, 0.042); 115 | --color-hint: rgba(0, 0, 0, 0.055); 116 | --color-overlay: rgba(0, 0, 0, 0.2); 117 | --color-divider: #e8e8e8; 118 | --color-panel-contrast: #ffffff; 119 | --color-panel-overlay: rgba(255, 255, 255, 0.82); 120 | --color-panel: #fdfdfd; 121 | --color-focus: #004094; 122 | --color-selected: #2f80ed; 123 | --color-selected-contrast: #ffffff; 124 | --color-selection-fill: #1e90ff06; 125 | --color-selection-stroke: #2f80ed; 126 | --color-text-0: #1d1d1d; 127 | --color-text-1: #2d2d2d; 128 | --color-text-3: #a4a5a7; 129 | --color-text-shadow: #ffffff; 130 | --color-primary: #2f80ed; 131 | --color-warn: #d10b0b; 132 | --color-text: #000000; 133 | --color-laser: #ff0000; 134 | 135 | --shadow-1: 0px 1px 2px rgba(0, 0, 0, 0.25), 0px 1px 3px rgba(0, 0, 0, 0.09); 136 | --shadow-2: 0px 0px 2px rgba(0, 0, 0, 0.16), 0px 2px 3px rgba(0, 0, 0, 0.24), 137 | 0px 2px 6px rgba(0, 0, 0, 0.1), inset 0px 0px 0px 1px var(--color-panel-contrast); 138 | --shadow-3: 0px 1px 2px rgba(0, 0, 0, 0.28), 0px 2px 6px rgba(0, 0, 0, 0.14), 139 | inset 0px 0px 0px 1px var(--color-panel-contrast); 140 | --shadow-4: 0px 0px 3px rgba(0, 0, 0, 0.19), 0px 5px 4px rgba(0, 0, 0, 0.16), 141 | 0px 2px 16px rgba(0, 0, 0, 0.06), inset 0px 0px 0px 1px var(--color-panel-contrast); 142 | } 143 | 144 | .tl-theme__dark { 145 | --color-accent: #e64a4a; 146 | --color-background: #212529; 147 | --color-brush-fill: rgba(180, 180, 180, 0.05); 148 | --color-brush-stroke: rgba(180, 180, 180, 0.25); 149 | --color-grid: #909090; 150 | --color-low: #2c3136; 151 | --color-low-border: #30363b; 152 | --color-culled: blue; 153 | --color-muted-none: rgba(255, 255, 255, 0); 154 | --color-muted-0: rgba(255, 255, 255, 0.02); 155 | --color-muted-1: rgba(255, 255, 255, 0.1); 156 | --color-muted-2: rgba(255, 255, 255, 0.05); 157 | --color-hint: rgba(255, 255, 255, 0.1); 158 | --color-overlay: rgba(0, 0, 0, 0.35); 159 | --color-divider: #49555f; 160 | --color-panel-contrast: #49555f; 161 | --color-panel: #363d44; 162 | --color-panel-overlay: rgba(54, 61, 68, 0.82); 163 | --color-focus: #a5c3f3; 164 | --color-selected: #4285f4; 165 | --color-selected-contrast: #ffffff; 166 | --color-selection-fill: rgba(38, 150, 255, 0.05); 167 | --color-selection-stroke: #2f80ed; 168 | --color-text-0: #f0eded; 169 | --color-text-1: #d9d9d9; 170 | --color-text-3: #6d747b; 171 | --color-text-shadow: #292f35; 172 | --color-primary: #2f80ed; 173 | --color-warn: #ef6464; 174 | --color-text: #f8f9fa; 175 | --color-laser: #ff0000; 176 | 177 | --shadow-1: 0px 1px 2px #00000029, 0px 1px 3px #00000038, 178 | inset 0px 0px 0px 1px var(--color-panel-contrast); 179 | --shadow-2: 0px 1px 3px #00000077, 0px 2px 6px #00000055, 180 | inset 0px 0px 0px 1px var(--color-panel-contrast); 181 | --shadow-3: 0px 1px 3px #00000077, 0px 2px 12px rgba(0, 0, 0, 0.22), 182 | inset 0px 0px 0px 1px var(--color-panel-contrast); 183 | } 184 | 185 | .tl-counter-scaled { 186 | transform: scale(var(--tl-scale)); 187 | transform-origin: top left; 188 | width: calc(100% * var(--tl-zoom)); 189 | height: calc(100% * var(--tl-zoom)); 190 | } 191 | 192 | .tl-container, 193 | .tl-container * { 194 | -webkit-touch-callout: none; 195 | -webkit-tap-highlight-color: transparent; 196 | scrollbar-highlight-color: transparent; 197 | -webkit-user-select: none; 198 | user-select: none; 199 | box-sizing: border-box; 200 | outline: none; 201 | } 202 | 203 | .tl-container a { 204 | -webkit-touch-callout: initial; 205 | } 206 | 207 | .tl-container:focus-within { 208 | outline: 1px solid var(--color-low); 209 | } 210 | 211 | input, 212 | *[contenteditable], 213 | *[contenteditable] * { 214 | -webkit-user-select: text; 215 | } 216 | 217 | /* -------------------------------------------------- */ 218 | /* Canvas */ 219 | /* -------------------------------------------------- */ 220 | 221 | .tl-canvas { 222 | position: absolute; 223 | top: 0px; 224 | left: 0px; 225 | width: 100%; 226 | height: 100%; 227 | color: var(--color-text); 228 | z-index: var(--layer-canvas); 229 | cursor: var(--tl-cursor); 230 | overflow: clip; 231 | content-visibility: auto; 232 | touch-action: none; 233 | contain: strict; 234 | } 235 | 236 | .tl-fixed-layer { 237 | position: absolute; 238 | top: 0px; 239 | left: 0px; 240 | width: 100%; 241 | height: 100%; 242 | contain: strict; 243 | pointer-events: none; 244 | } 245 | 246 | .tl-shapes { 247 | position: relative; 248 | z-index: var(--layer-shapes); 249 | } 250 | 251 | .tl-overlays { 252 | position: relative; 253 | z-index: var(--layer-overlays); 254 | } 255 | 256 | .tl-overlays__item { 257 | position: absolute; 258 | top: 0px; 259 | left: 0px; 260 | height: var(--tl-dpr-multiple-px); 261 | width: var(--tl-dpr-multiple-px); 262 | overflow: visible; 263 | pointer-events: none; 264 | transform-origin: top left; 265 | } 266 | 267 | .tl-svg-context { 268 | position: absolute; 269 | top: 0px; 270 | left: 0px; 271 | height: var(--tl-dpr-multiple-px); 272 | width: var(--tl-dpr-multiple-px); 273 | pointer-events: none; 274 | } 275 | 276 | .tl-positioned { 277 | position: absolute; 278 | top: 0px; 279 | left: 0px; 280 | transform-origin: top left; 281 | } 282 | 283 | /* ------------------- Background ------------------- */ 284 | 285 | .tl-background { 286 | position: absolute; 287 | background-color: var(--color-background); 288 | inset: 0px; 289 | z-index: var(--layer-background); 290 | } 291 | 292 | /* --------------------- Grid Layer --------------------- */ 293 | 294 | .tl-grid { 295 | position: absolute; 296 | top: 0px; 297 | left: 0px; 298 | width: 100%; 299 | height: 100%; 300 | touch-action: none; 301 | pointer-events: none; 302 | z-index: var(--layer-grid); 303 | contain: strict; 304 | } 305 | 306 | .tl-grid-dot { 307 | fill: var(--color-grid); 308 | } 309 | 310 | /* --------------------- Layers --------------------- */ 311 | 312 | .tl-html-layer { 313 | position: absolute; 314 | top: 0px; 315 | left: 0px; 316 | width: 1px; 317 | height: 1px; 318 | contain: layout style size; 319 | } 320 | 321 | /* ---------------------- Brush --------------------- */ 322 | 323 | .tl-brush { 324 | stroke-width: calc(var(--tl-scale) * 1px); 325 | contain: size layout; 326 | } 327 | 328 | .tl-brush__default { 329 | stroke: var(--color-brush-stroke); 330 | fill: var(--color-brush-fill); 331 | } 332 | 333 | /* -------------------- Scribble -------------------- */ 334 | 335 | .tl-scribble { 336 | stroke-linejoin: round; 337 | stroke-linecap: round; 338 | pointer-events: none; 339 | contain: size layout; 340 | } 341 | 342 | /* ---------------------- Shape --------------------- */ 343 | 344 | .tl-shape { 345 | position: absolute; 346 | pointer-events: none; 347 | overflow: visible; 348 | transform-origin: top left; 349 | contain: size layout; 350 | } 351 | 352 | .tl-shape__culled { 353 | position: relative; 354 | background-color: var(--color-culled); 355 | } 356 | 357 | /* ---------------- Shape Containers ---------------- */ 358 | 359 | .tl-svg-container { 360 | position: absolute; 361 | top: 0px; 362 | left: 0px; 363 | width: 100%; 364 | height: 100%; 365 | pointer-events: none; 366 | stroke-linecap: round; 367 | stroke-linejoin: round; 368 | transform-origin: top left; 369 | overflow: visible; 370 | } 371 | 372 | .tl-html-container { 373 | position: absolute; 374 | top: 0px; 375 | left: 0px; 376 | width: 100%; 377 | height: 100%; 378 | pointer-events: none; 379 | stroke-linecap: round; 380 | stroke-linejoin: round; 381 | /* content-visibility: auto; */ 382 | transform-origin: top left; 383 | color: inherit; 384 | } 385 | 386 | /* --------------- Overlay Stack --------------- */ 387 | 388 | /* back of the stack, behind user's stuff */ 389 | .tl-collaborator__scribble { 390 | z-index: 10; 391 | } 392 | 393 | .tl-collaborator__brush { 394 | z-index: 20; 395 | } 396 | 397 | .tl-collaborator__shape-indicator { 398 | z-index: 30; 399 | } 400 | 401 | .tl-user-scribble { 402 | z-index: 40; 403 | } 404 | 405 | .tl-user-brush { 406 | z-index: 50; 407 | } 408 | 409 | .tl-user-indicator__selected { 410 | z-index: 60; 411 | } 412 | 413 | .tl-user-indicator__hovered { 414 | z-index: 70; 415 | } 416 | 417 | .tl-user-handles { 418 | z-index: 80; 419 | } 420 | 421 | .tl-user-snapline { 422 | z-index: 90; 423 | } 424 | 425 | .tl-selection__fg { 426 | pointer-events: none; 427 | z-index: 100; 428 | } 429 | 430 | .tl-user-indicator__hint { 431 | z-index: 110; 432 | stroke-width: calc(2.5px * var(--tl-scale)); 433 | } 434 | 435 | /* behind collaborator cursor */ 436 | .tl-collaborator__cursor-hint { 437 | z-index: 120; 438 | } 439 | 440 | .tl-collaborator__cursor { 441 | z-index: 130; 442 | } 443 | 444 | .tl-cursor { 445 | overflow: visible; 446 | } 447 | 448 | /* -------------------- Indicator ------------------- */ 449 | 450 | .tl-shape-indicator { 451 | transform-origin: top left; 452 | fill: none; 453 | stroke-width: calc(1.5px * var(--tl-scale)); 454 | contain: size; 455 | } 456 | 457 | /* ------------------ SelectionBox ------------------ */ 458 | 459 | .tl-selection__bg { 460 | position: absolute; 461 | top: 0px; 462 | left: 0px; 463 | transform-origin: top left; 464 | background-color: transparent; 465 | pointer-events: all; 466 | } 467 | 468 | .tl-selection__fg__outline { 469 | fill: none; 470 | pointer-events: none; 471 | stroke: var(--color-selection-stroke); 472 | stroke-width: calc(1.5px * var(--tl-scale)); 473 | } 474 | 475 | .tl-corner-handle { 476 | pointer-events: none; 477 | stroke: var(--color-selection-stroke); 478 | fill: var(--color-background); 479 | stroke-width: calc(1.5px * var(--tl-scale)); 480 | } 481 | 482 | .tl-text-handle { 483 | pointer-events: none; 484 | fill: var(--color-selection-stroke); 485 | } 486 | 487 | .tl-corner-crop-handle { 488 | pointer-events: none; 489 | fill: none; 490 | stroke: var(--color-selection-stroke); 491 | } 492 | 493 | .tl-corner-crop-edge-handle { 494 | pointer-events: none; 495 | fill: none; 496 | stroke: var(--color-selection-stroke); 497 | } 498 | 499 | .tl-rotate-handle { 500 | stroke: var(--color-selection-stroke); 501 | fill: var(--color-background); 502 | stroke-width: calc(1.5px * var(--tl-scale)); 503 | pointer-events: all; 504 | } 505 | 506 | .tl-mobile-rotate__bg { 507 | pointer-events: all; 508 | cursor: var(--tl-cursor-grab); 509 | } 510 | 511 | .tl-mobile-rotate__fg { 512 | pointer-events: none; 513 | stroke: var(--color-selection-stroke); 514 | fill: var(--color-background); 515 | stroke-width: calc(1.5px * var(--tl-scale)); 516 | } 517 | 518 | .tl-transparent { 519 | fill: transparent; 520 | stroke: transparent; 521 | } 522 | 523 | /* --------------------- Handles -------------------- */ 524 | 525 | .tl-handle { 526 | pointer-events: all; 527 | } 528 | 529 | .tl-handle__bg { 530 | fill: transparent; 531 | stroke: transparent; 532 | pointer-events: all; 533 | cursor: var(--tl-cursor-grabbing); 534 | } 535 | 536 | .tl-handle__fg { 537 | fill: var(--color-background); 538 | stroke: var(--color-selection-stroke); 539 | stroke-width: calc(1.5px * var(--tl-scale)); 540 | pointer-events: none; 541 | } 542 | 543 | .tl-handle__create { 544 | opacity: 0; 545 | } 546 | .tl-handle__create:hover { 547 | opacity: 1; 548 | } 549 | 550 | .tl-handle__bg:active { 551 | fill: none; 552 | } 553 | 554 | .tl-handle__bg:hover { 555 | cursor: var(--tl-cursor-grab); 556 | fill: var(--color-selection-fill); 557 | } 558 | 559 | @media (pointer: coarse) { 560 | .tl-handle__bg:active { 561 | fill: var(--color-selection-fill); 562 | } 563 | 564 | .tl-handle__create { 565 | opacity: 1; 566 | } 567 | } 568 | 569 | /* ------------------ Bounds Detail ----------------- */ 570 | 571 | .tl-image, 572 | .tl-video { 573 | object-fit: cover; 574 | background-size: cover; 575 | width: 100%; 576 | height: 100%; 577 | } 578 | 579 | .tl-image-container, 580 | .tl-video-container, 581 | .tl-embed-container { 582 | width: 100%; 583 | height: 100%; 584 | pointer-events: all; 585 | /* background-color: var(--color-background); */ 586 | 587 | display: flex; 588 | justify-content: center; 589 | align-items: center; 590 | } 591 | 592 | .tl-image__tg { 593 | --scale: calc(min(2, var(--tl-scale))); 594 | position: absolute; 595 | top: calc(var(--scale) * 8px); 596 | right: calc(var(--scale) * 8px); 597 | font-size: 10px; 598 | transform-origin: top right; 599 | background-color: var(--color-background); 600 | padding: 2px 4px; 601 | border-radius: var(--radius-1); 602 | } 603 | 604 | /* --------------------- Nametag -------------------- */ 605 | 606 | .tl-collaborator-cursor { 607 | position: absolute; 608 | } 609 | 610 | .tl-nametag { 611 | position: absolute; 612 | top: 16px; 613 | left: 13px; 614 | width: fit-content; 615 | height: fit-content; 616 | max-width: 120px; 617 | padding: 3px 6px; 618 | white-space: nowrap; 619 | position: absolute; 620 | overflow: hidden; 621 | text-overflow: ellipsis; 622 | font-size: 12px; 623 | font-family: var(--font-body); 624 | border-radius: var(--radius-2); 625 | color: var(--color-selected-contrast); 626 | } 627 | 628 | .tl-nametag-title { 629 | position: absolute; 630 | top: -2px; 631 | left: 13px; 632 | width: fit-content; 633 | height: fit-content; 634 | padding: 0px 6px; 635 | max-width: 120px; 636 | white-space: nowrap; 637 | position: absolute; 638 | overflow: hidden; 639 | text-overflow: ellipsis; 640 | font-size: 12px; 641 | font-family: var(--font-body); 642 | text-shadow: var(--tl-text-outline); 643 | color: var(--color-selected-contrast); 644 | } 645 | 646 | .tl-nametag-chat { 647 | position: absolute; 648 | top: 16px; 649 | left: 13px; 650 | width: fit-content; 651 | height: fit-content; 652 | color: var(--color-selected-contrast); 653 | white-space: nowrap; 654 | position: absolute; 655 | padding: 3px 6px; 656 | font-size: 12px; 657 | font-family: var(--font-body); 658 | opacity: 1; 659 | border-radius: var(--radius-2); 660 | } 661 | 662 | .tl-cursor-chat { 663 | position: absolute; 664 | color: var(--color-selected-contrast); 665 | white-space: nowrap; 666 | padding: 3px 6px; 667 | font-size: 12px; 668 | font-family: var(--font-body); 669 | pointer-events: none; 670 | z-index: var(--layer-cursor); 671 | margin-top: 16px; 672 | margin-left: 13px; 673 | opacity: 1; 674 | border: none; 675 | user-select: text; 676 | border-radius: var(--radius-2); 677 | } 678 | 679 | .tl-cursor-chat .tl-cursor-chat__bubble { 680 | padding-right: 12px; 681 | } 682 | 683 | .tl-cursor-chat::selection { 684 | background: var(--color-selected); 685 | color: var(--color-selected-contrast); 686 | text-shadow: none; 687 | } 688 | 689 | .tl-cursor-chat-fade { 690 | /* Setting to zero causes it to immediately disappear */ 691 | /* Setting to near-zero causes it to fade out gradually */ 692 | opacity: 0.0001; 693 | transition: opacity 5s ease-in-out; 694 | } 695 | 696 | .tl-cursor-chat::placeholder { 697 | color: var(--color-selected-contrast); 698 | opacity: 0.7; 699 | } 700 | 701 | /* -------------------------------------------------- */ 702 | /* Spinner */ 703 | /* -------------------------------------------------- */ 704 | 705 | @keyframes spinner { 706 | to { 707 | transform: rotate(360deg); 708 | } 709 | } 710 | 711 | .tl-spinner::after { 712 | content: ''; 713 | box-sizing: border-box; 714 | position: absolute; 715 | top: 50%; 716 | left: 50%; 717 | width: 20px; 718 | height: 20px; 719 | margin-top: -10px; 720 | margin-left: -10px; 721 | border-radius: 50%; 722 | border: 2px solid #ccc; 723 | border-top-color: #000; 724 | animation: spinner 0.6s linear infinite; 725 | pointer-events: none; 726 | } 727 | 728 | /* -------------------- IconShape ------------------- */ 729 | 730 | .tl-iconshape__icon { 731 | pointer-events: all; 732 | width: 100%; 733 | height: 100%; 734 | } 735 | 736 | .tl-icon-preview { 737 | width: 14px; 738 | height: 14px; 739 | } 740 | 741 | /* ------------------- Text Shape ------------------- */ 742 | 743 | .tl-text-shape__wrapper { 744 | position: relative; 745 | font-weight: normal; 746 | min-width: 1px; 747 | padding: 0px; 748 | margin: 0px; 749 | border: none; 750 | height: 100%; 751 | font-variant: normal; 752 | font-style: normal; 753 | pointer-events: all; 754 | white-space: pre-wrap; 755 | overflow-wrap: break-word; 756 | text-shadow: var(--tl-text-outline); 757 | } 758 | 759 | .tl-text-shape__wrapper[data-align='start'] { 760 | text-align: left; 761 | } 762 | 763 | .tl-text-shape__wrapper[data-align='middle'] { 764 | text-align: center; 765 | } 766 | 767 | .tl-text-shape__wrapper[data-align='end'] { 768 | text-align: right; 769 | } 770 | 771 | .tl-text-shape__wrapper[data-font='draw'] { 772 | font-family: var(--tl-font-draw); 773 | } 774 | 775 | .tl-text-shape__wrapper[data-font='sans'] { 776 | font-family: var(--tl-font-sans); 777 | } 778 | 779 | .tl-text-shape__wrapper[data-font='serif'] { 780 | font-family: var(--tl-font-serif); 781 | } 782 | 783 | .tl-text-shape__wrapper[data-font='mono'] { 784 | font-family: var(--tl-font-mono); 785 | } 786 | 787 | .tl-text-shape__wrapper[data-isediting='true'] .tl-text-content { 788 | opacity: 0; 789 | } 790 | 791 | .tl-text { 792 | /* remove overflow from textarea on windows */ 793 | margin: 0px; 794 | padding: 0px; 795 | border: 0px; 796 | color: inherit; 797 | caret-color: var(--color-text); 798 | background: none; 799 | border-image: none; 800 | font-size: inherit; 801 | font-family: inherit; 802 | font-weight: inherit; 803 | line-height: inherit; 804 | font-variant: inherit; 805 | font-style: inherit; 806 | text-align: inherit; 807 | letter-spacing: inherit; 808 | text-shadow: inherit; 809 | outline: none; 810 | white-space: pre-wrap; 811 | word-wrap: break-word; 812 | overflow-wrap: break-word; 813 | pointer-events: all; 814 | text-rendering: auto; 815 | text-transform: none; 816 | text-indent: 0px; 817 | display: inline-block; 818 | appearance: auto; 819 | column-count: initial !important; 820 | writing-mode: horizontal-tb !important; 821 | word-spacing: 0px; 822 | } 823 | 824 | .tl-text-measure { 825 | position: absolute; 826 | z-index: 999999; 827 | top: -9999px; 828 | right: -9999px; 829 | opacity: 0; 830 | width: max-content; 831 | box-sizing: border-box; 832 | pointer-events: none; 833 | line-break: normal; 834 | white-space: pre-wrap; 835 | word-wrap: break-word; 836 | overflow-wrap: break-word; 837 | resize: none; 838 | border: none; 839 | user-select: none; 840 | contain: style paint; 841 | -webkit-user-select: none; 842 | } 843 | 844 | .tl-text-edit-container { 845 | position: relative; 846 | width: 100%; 847 | height: 100%; 848 | } 849 | 850 | .tl-text-input, 851 | .tl-text-content { 852 | position: absolute; 853 | top: 0px; 854 | left: 0px; 855 | width: 100%; 856 | height: 100%; 857 | min-width: 1px; 858 | min-height: 1px; 859 | overflow: visible; 860 | outline: none; 861 | } 862 | 863 | .tl-text-content { 864 | pointer-events: none; 865 | } 866 | 867 | .tl-text-input { 868 | resize: none; 869 | user-select: all; 870 | -webkit-user-select: text; 871 | overflow: hidden; 872 | cursor: var(--tl-cursor-text); 873 | } 874 | 875 | .tl-text-input::selection { 876 | background: var(--color-selected); 877 | color: var(--color-selected-contrast); 878 | text-shadow: none; 879 | } 880 | 881 | /* ------------------- Snap Lines ------------------- */ 882 | 883 | .tl-snap-line { 884 | stroke: var(--color-accent); 885 | stroke-width: calc(1px * var(--tl-scale)); 886 | fill: none; 887 | } 888 | 889 | .tl-snap-point { 890 | stroke: var(--color-accent); 891 | stroke-width: calc(1px * var(--tl-scale)); 892 | fill: none; 893 | } 894 | 895 | /* -------------------- Groups ------------------ */ 896 | 897 | .tl-group { 898 | stroke: var(--color-text); 899 | stroke-width: calc(1px * var(--tl-scale)); 900 | opacity: 0.5; 901 | } 902 | 903 | /* ------------------- Bookmark Shape ------------------- */ 904 | 905 | .tl-bookmark__container { 906 | width: 100%; 907 | height: 100%; 908 | position: relative; 909 | border: 1px solid var(--color-panel-contrast); 910 | background-color: var(--color-panel); 911 | border-radius: var(--radius-2); 912 | display: flex; 913 | flex-direction: column; 914 | overflow: hidden; 915 | } 916 | 917 | .tl-bookmark__image_container { 918 | flex: 1; 919 | overflow: hidden; 920 | border-top-left-radius: var(--radius-1); 921 | border-top-right-radius: var(--radius-1); 922 | width: 100%; 923 | height: 100%; 924 | display: flex; 925 | justify-content: flex-end; 926 | align-items: flex-start; 927 | } 928 | 929 | .tl-bookmark__image_container > .tl-hyperlink-button::after { 930 | background-color: var(--color-panel); 931 | } 932 | 933 | .tl-bookmark__placeholder { 934 | width: 100%; 935 | height: 100%; 936 | background-color: var(--color-muted-2); 937 | border-bottom: 1px solid var(--color-muted-2); 938 | } 939 | 940 | .tl-bookmark__image { 941 | width: 100%; 942 | height: 100%; 943 | object-fit: cover; 944 | object-position: center; 945 | } 946 | 947 | .tl-bookmark__copy_container { 948 | background-color: var(--color-muted); 949 | padding: var(--space-4); 950 | pointer-events: all; 951 | } 952 | 953 | .tl-bookmark__heading, 954 | .tl-bookmark__description, 955 | .tl-bookmark__link { 956 | margin: 0px; 957 | width: 100%; 958 | font-family: inherit; 959 | } 960 | 961 | .tl-bookmark__heading { 962 | font-size: 16px; 963 | font-weight: bold; 964 | padding-bottom: var(--space-2); 965 | margin: 8px 0px; 966 | } 967 | 968 | .tl-bookmark__description { 969 | font-size: 12px; 970 | padding-bottom: var(--space-4); 971 | } 972 | 973 | .tl-bookmark__link { 974 | font-size: 14px; 975 | pointer-events: all; 976 | z-index: 999; 977 | overflow: hidden; 978 | display: block; 979 | color: var(--color-text); 980 | text-overflow: ellipsis; 981 | text-decoration: none; 982 | color: var(--color-text-1); 983 | cursor: var(--tl-cursor-pointer); 984 | } 985 | 986 | .tl-bookmark__link:hover { 987 | color: var(--color-selected); 988 | } 989 | 990 | /* ---------------- Hyperlink Button ---------------- */ 991 | 992 | .tl-hyperlink-button { 993 | background: none; 994 | margin: 0px; 995 | position: absolute; 996 | top: 0px; 997 | right: 0px; 998 | height: 44px; 999 | width: 44px; 1000 | display: flex; 1001 | align-items: center; 1002 | justify-content: center; 1003 | z-index: 200; 1004 | font-size: 12px; 1005 | font-weight: 400; 1006 | color: var(--color-text-1); 1007 | padding: 13px; 1008 | cursor: var(--tl-cursor-pointer); 1009 | border: none; 1010 | outline: none; 1011 | pointer-events: all; 1012 | } 1013 | 1014 | .tl-hyperlink-button::after { 1015 | content: ''; 1016 | z-index: -1; 1017 | position: absolute; 1018 | right: 6px; 1019 | bottom: 6px; 1020 | display: block; 1021 | width: calc(100% - 12px); 1022 | height: calc(100% - 12px); 1023 | border-radius: var(--radius-1); 1024 | background-color: var(--color-background); 1025 | pointer-events: none; 1026 | } 1027 | 1028 | .tl-hyperlink-button:hover { 1029 | color: var(--color-selected); 1030 | } 1031 | 1032 | .tl-hyperlink-button:focus-visible { 1033 | color: var(--color-selected); 1034 | } 1035 | 1036 | .tl-hyperlink-button__icon { 1037 | width: 18px; 1038 | height: 18px; 1039 | background-color: currentColor; 1040 | pointer-events: none; 1041 | } 1042 | 1043 | .tl-hyperlink-button__hidden { 1044 | display: none; 1045 | } 1046 | 1047 | /* ---------------- Geo shape ---------------- */ 1048 | 1049 | .tl-text-label { 1050 | display: flex; 1051 | justify-content: center; 1052 | align-items: center; 1053 | color: var(--color-text); 1054 | text-shadow: var(--tl-text-outline); 1055 | line-height: inherit; 1056 | position: absolute; 1057 | inset: 0px; 1058 | z-index: 10; 1059 | } 1060 | 1061 | .tl-text-label[data-isediting='true'] .tl-text-content { 1062 | opacity: 0; 1063 | } 1064 | 1065 | .tl-text-label[data-hastext='false'][data-isediting='false'] > .tl-text-label__inner { 1066 | width: 40px; 1067 | height: 40px; 1068 | } 1069 | 1070 | .tl-text-label[data-hastext='true'][data-isediting='false'] .tl-text-content { 1071 | pointer-events: all; 1072 | } 1073 | 1074 | .tl-text-label__inner { 1075 | position: relative; 1076 | width: fit-content; 1077 | height: fit-content; 1078 | display: flex; 1079 | align-items: center; 1080 | justify-content: center; 1081 | pointer-events: all; 1082 | min-height: auto; 1083 | } 1084 | 1085 | .tl-text-label__inner > .tl-text { 1086 | position: relative; 1087 | top: 0px; 1088 | left: 0px; 1089 | padding: 16px; 1090 | height: fit-content; 1091 | width: fit-content; 1092 | border-radius: var(--radius-1); 1093 | max-width: 100%; 1094 | z-index: 3; 1095 | } 1096 | 1097 | .tl-text-label__inner > .tl-text-input { 1098 | position: absolute; 1099 | top: 0px; 1100 | left: 0px; 1101 | width: 100%; 1102 | height: 100%; 1103 | padding: 16px; 1104 | z-index: 4; 1105 | } 1106 | 1107 | .tl-text-label[data-textwrap='true'] > .tl-text-label__inner { 1108 | max-width: 100%; 1109 | } 1110 | 1111 | .tl-text-label[data-isediting='true'] { 1112 | background-color: transparent; 1113 | min-height: auto; 1114 | } 1115 | 1116 | .tl-text-label[data-isediting='true'] p { 1117 | opacity: 0; 1118 | } 1119 | 1120 | .tl-text-label[data-align='start'], 1121 | .tl-text-label[data-align='start-legacy'] { 1122 | text-align: left; 1123 | } 1124 | 1125 | .tl-text-label[data-align='middle'], 1126 | .tl-text-label[data-align='middle-legacy'] { 1127 | text-align: center; 1128 | } 1129 | 1130 | .tl-text-label[data-align='end'], 1131 | .tl-text-label[data-align='end-legacy'] { 1132 | text-align: right; 1133 | } 1134 | 1135 | .tl-arrow-hint { 1136 | stroke: var(--color-text-1); 1137 | fill: none; 1138 | stroke-linecap: round; 1139 | overflow: visible; 1140 | } 1141 | 1142 | .tl-arrow-label[data-font='draw'], 1143 | .tl-text-label[data-font='draw'] { 1144 | font-family: var(--tl-font-draw); 1145 | } 1146 | 1147 | .tl-arrow-label[data-font='sans'], 1148 | .tl-text-label[data-font='sans'] { 1149 | font-family: var(--tl-font-sans); 1150 | } 1151 | 1152 | .tl-arrow-label[data-font='serif'], 1153 | .tl-text-label[data-font='serif'] { 1154 | font-family: var(--tl-font-serif); 1155 | } 1156 | 1157 | .tl-arrow-label[data-font='mono'], 1158 | .tl-text-label[data-font='mono'] { 1159 | font-family: var(--tl-font-mono); 1160 | } 1161 | 1162 | /* ------------------- Arrow Shape ------------------ */ 1163 | 1164 | .tl-arrow-label { 1165 | position: absolute; 1166 | top: -1px; 1167 | left: -1px; 1168 | width: 2px; 1169 | height: 2px; 1170 | padding: 0px; 1171 | display: flex; 1172 | justify-content: center; 1173 | align-items: center; 1174 | color: var(--color-text); 1175 | text-shadow: var(--tl-text-outline); 1176 | } 1177 | 1178 | .tl-arrow-label[data-isediting='true'] p { 1179 | opacity: 0; 1180 | } 1181 | 1182 | .tl-arrow-label[data-isediting='true'] > .tl-arrow-label__inner { 1183 | background-color: var(--color-background); 1184 | } 1185 | 1186 | .tl-arrow-label__inner { 1187 | border-radius: var(--radius-1); 1188 | box-sizing: content-box; 1189 | position: relative; 1190 | height: max-content; 1191 | width: max-content; 1192 | pointer-events: all; 1193 | display: flex; 1194 | justify-content: center; 1195 | align-items: center; 1196 | } 1197 | 1198 | .tl-arrow-label p, 1199 | .tl-arrow-label textarea { 1200 | margin: 0px; 1201 | padding: 0px; 1202 | border: 0px; 1203 | color: inherit; 1204 | caret-color: var(--color-text); 1205 | background: none; 1206 | border-image: none; 1207 | font-size: inherit; 1208 | font-family: inherit; 1209 | font-weight: inherit; 1210 | line-height: inherit; 1211 | font-variant: inherit; 1212 | font-style: inherit; 1213 | text-align: inherit; 1214 | letter-spacing: inherit; 1215 | text-shadow: inherit; 1216 | outline: none; 1217 | white-space: pre-wrap; 1218 | word-wrap: break-word; 1219 | overflow-wrap: break-word; 1220 | pointer-events: all; 1221 | text-rendering: auto; 1222 | text-transform: none; 1223 | text-indent: 0px; 1224 | display: inline-block; 1225 | appearance: auto; 1226 | column-count: initial !important; 1227 | writing-mode: horizontal-tb !important; 1228 | word-spacing: 0px; 1229 | } 1230 | 1231 | .tl-arrow-label p { 1232 | position: relative; 1233 | height: max-content; 1234 | z-index: 2; 1235 | padding: 4px; 1236 | overflow: visible; 1237 | } 1238 | 1239 | .tl-arrow-label textarea { 1240 | z-index: 3; 1241 | margin: 0px; 1242 | padding: 4px; 1243 | height: 100%; 1244 | width: 100%; 1245 | position: absolute; 1246 | resize: none; 1247 | border: 0px; 1248 | user-select: all; 1249 | -webkit-user-select: text; 1250 | caret-color: var(--color-text); 1251 | border-image: none; 1252 | /* Don't allow textarea to be zero width */ 1253 | min-width: 4px; 1254 | } 1255 | 1256 | /* -------------------- NoteShape ------------------- */ 1257 | 1258 | .tl-note__container { 1259 | position: relative; 1260 | width: 100%; 1261 | height: 100%; 1262 | border-radius: var(--radius-2); 1263 | box-shadow: var(--shadow-1); 1264 | overflow: hidden; 1265 | border-color: currentColor; 1266 | border-style: solid; 1267 | border-width: 1px; 1268 | } 1269 | 1270 | .tl-note__container .tl-text-label { 1271 | text-shadow: none; 1272 | } 1273 | 1274 | .tl-note__scrim { 1275 | position: absolute; 1276 | z-index: 1; 1277 | top: 0px; 1278 | left: 0px; 1279 | width: 100%; 1280 | height: 100%; 1281 | background-color: var(--color-background); 1282 | opacity: 0.28; 1283 | } 1284 | 1285 | .tl-loading { 1286 | background-color: var(--color-background); 1287 | color: var(--color-text-1); 1288 | height: 100%; 1289 | width: 100%; 1290 | display: flex; 1291 | flex-direction: column; 1292 | justify-content: center; 1293 | align-items: center; 1294 | gap: var(--space-2); 1295 | font-size: 14px; 1296 | font-weight: 500; 1297 | opacity: 0; 1298 | animation: fade-in 0.2s ease-in-out forwards; 1299 | animation-delay: 0.2s; 1300 | } 1301 | 1302 | @keyframes fade-in { 1303 | 0% { 1304 | opacity: 0; 1305 | } 1306 | 100% { 1307 | opacity: 1; 1308 | } 1309 | } 1310 | 1311 | /* -------------------- FrameShape ------------------- */ 1312 | 1313 | .tl-frame__body { 1314 | stroke-width: calc(1px * var(--tl-scale)); 1315 | } 1316 | 1317 | .tl-frame__hitarea { 1318 | border-style: solid; 1319 | border-width: calc(8px * var(--tl-scale)); 1320 | border-color: transparent; 1321 | background: none; 1322 | pointer-events: stroke; 1323 | box-sizing: border-box; 1324 | top: calc(-8px * var(--tl-scale)); 1325 | left: calc(-8px * var(--tl-scale)); 1326 | width: calc(100% + calc(16px * var(--tl-scale))); 1327 | height: calc(100% + calc(16px * var(--tl-scale))); 1328 | z-index: 1; 1329 | position: absolute; 1330 | } 1331 | 1332 | .tl-frame-heading { 1333 | display: flex; 1334 | align-items: center; 1335 | position: absolute; 1336 | transform-origin: 0% 100%; 1337 | overflow: hidden; 1338 | max-width: 100%; 1339 | min-width: 32px; 1340 | height: auto; 1341 | font-size: 12px; 1342 | padding-bottom: 4px; 1343 | pointer-events: all; 1344 | } 1345 | 1346 | .tl-frame-heading-hit-area { 1347 | pointer-events: all; 1348 | /* scale from bottom left corner so we can pin it to the top left corner of the frame */ 1349 | transform-origin: 0% 100%; 1350 | display: flex; 1351 | height: 100%; 1352 | width: 100%; 1353 | align-items: center; 1354 | border-radius: var(--radius-1); 1355 | background-color: var(--color-background); 1356 | } 1357 | 1358 | .tl-frame-label { 1359 | pointer-events: all; 1360 | overflow: hidden; 1361 | text-overflow: ellipsis; 1362 | padding: var(--space-3) var(--space-3); 1363 | position: relative; 1364 | font-size: inherit; 1365 | white-space: pre; 1366 | border: 1px solid transparent; 1367 | } 1368 | 1369 | .tl-frame-label__editing { 1370 | color: transparent; 1371 | white-space: pre; 1372 | width: auto; 1373 | overflow: visible; 1374 | background-color: var(--color-panel); 1375 | border-radius: var(--radius-1); 1376 | border-color: var(--color-selected); 1377 | } 1378 | 1379 | .tl-frame-name-input { 1380 | position: absolute; 1381 | top: 0px; 1382 | left: 0px; 1383 | border: none; 1384 | background: none; 1385 | outline: none; 1386 | padding: var(--space-3) var(--space-3); 1387 | inset: 0px; 1388 | font-size: inherit; 1389 | font-family: inherit; 1390 | font-weight: inherit; 1391 | width: 100%; 1392 | color: var(--color-text-1); 1393 | border-radius: var(--radius-1); 1394 | user-select: all; 1395 | -webkit-user-select: text; 1396 | white-space: pre; 1397 | cursor: var(--tl-cursor-text); 1398 | } 1399 | 1400 | /* If mobile use 16px as font size */ 1401 | /* On iOS, font size under 16px in an input will make the page zoom into the input 🤦♂️ */ 1402 | /* https://css-tricks.com/16px-or-larger-text-prevents-ios-form-zoom/ */ 1403 | @media (max-width: 600px) { 1404 | .tl-frame-heading { 1405 | font-size: 16px; 1406 | } 1407 | } 1408 | 1409 | /* ------------------ iFrames Detail ----------------- */ 1410 | 1411 | .tl-embed { 1412 | border: none; 1413 | border-radius: var(--radius-2); 1414 | } 1415 | 1416 | /* ------------------- Code Editor ------------------ */ 1417 | 1418 | .tl-image__button { 1419 | padding: 4px 8px; 1420 | color: var(--color-text); 1421 | background-color: var(--color-panel); 1422 | border-radius: var(--radius-2); 1423 | box-shadow: var(--shadow-1); 1424 | pointer-events: all; 1425 | cursor: var(--tl-cursor-pointer); 1426 | outline: none; 1427 | display: flex; 1428 | } 1429 | 1430 | .tl-image__button:disabled { 1431 | opacity: 0.5; 1432 | pointer-events: none; 1433 | } 1434 | 1435 | .tl-image__toolbox { 1436 | position: absolute; 1437 | top: 0px; 1438 | left: 0px; 1439 | display: flex; 1440 | justify-content: flex-end; 1441 | align-items: flex-end; 1442 | padding: 10px; 1443 | } 1444 | 1445 | .tl-image__toolbox__hidden { 1446 | display: none; 1447 | } 1448 | 1449 | /* -------------- Shape Error Boundary -------------- */ 1450 | 1451 | .tl-shape-error-boundary { 1452 | width: 100%; 1453 | height: 100%; 1454 | background-color: var(--color-muted-1); 1455 | border-width: calc(1px * var(--tl-scale)); 1456 | border-color: var(--color-muted-1); 1457 | border-style: solid; 1458 | border-radius: calc(var(--radius-1) * var(--tl-scale)); 1459 | display: flex; 1460 | flex-direction: column; 1461 | align-items: center; 1462 | justify-content: center; 1463 | text-align: left; 1464 | position: relative; 1465 | pointer-events: all; 1466 | overflow: hidden; 1467 | padding: var(--space-2); 1468 | } 1469 | 1470 | .tl-shape-error-boundary::before { 1471 | transform: scale(var(--tl-scale)); 1472 | content: 'Error'; 1473 | font-size: 12px; 1474 | font-family: inherit; 1475 | color: var(--color-text-0); 1476 | } 1477 | 1478 | /* ----------------- Error Boundary ----------------- */ 1479 | 1480 | .tl-error-boundary { 1481 | width: 100%; 1482 | height: 100%; 1483 | display: flex; 1484 | align-items: center; 1485 | justify-content: center; 1486 | padding: var(--space-4); 1487 | background-color: var(--color-background); 1488 | color: var(--color-text-1); 1489 | position: absolute; 1490 | z-index: 600; 1491 | } 1492 | 1493 | .tl-error-boundary__overlay { 1494 | position: absolute; 1495 | inset: 0px; 1496 | z-index: 500; 1497 | background-color: var(--color-overlay); 1498 | } 1499 | 1500 | .tl-error-boundary__content * { 1501 | user-select: all; 1502 | -webkit-user-select: text; 1503 | pointer-events: all; 1504 | } 1505 | 1506 | .tl-error-boundary__canvas { 1507 | pointer-events: none; 1508 | position: absolute; 1509 | inset: 0px; 1510 | z-index: -1; 1511 | } 1512 | /* some browsers seem to have some weird interactions between stacking contexts 1513 | and pointer-events. this ::after pseudo element covers the canvas and prevents 1514 | it from receiving any pointer events or affecting the cursor. */ 1515 | .tl-error-boundary__canvas::after { 1516 | content: ' '; 1517 | display: block; 1518 | position: absolute; 1519 | inset: 0px; 1520 | z-index: 600; 1521 | pointer-events: all; 1522 | } 1523 | 1524 | .tl-error-boundary__content { 1525 | width: fit-content; 1526 | height: fit-content; 1527 | max-width: 100%; 1528 | width: 400px; 1529 | max-height: 100%; 1530 | background-color: var(--color-panel); 1531 | padding: 16px; 1532 | border-radius: 16px; 1533 | box-shadow: var(--shadow-2); 1534 | font-size: 14px; 1535 | font-weight: 400; 1536 | display: flex; 1537 | flex-direction: column; 1538 | overflow: auto; 1539 | z-index: 600; 1540 | gap: 12px; 1541 | } 1542 | 1543 | .tl-error-boundary__content__expanded { 1544 | width: 600px; 1545 | } 1546 | 1547 | .tl-error-boundary__content h2 { 1548 | font-size: 16px; 1549 | margin: 0px; 1550 | font-weight: 500; 1551 | } 1552 | 1553 | .tl-error-boundary__content p { 1554 | line-height: 1.5; 1555 | margin: 0px; 1556 | } 1557 | 1558 | .tl-error-boundary__content pre { 1559 | background-color: var(--color-muted-2); 1560 | padding: var(--space-5); 1561 | border-radius: var(--radius-2); 1562 | overflow: auto; 1563 | font-size: 12px; 1564 | max-height: 320px; 1565 | 1566 | } 1567 | 1568 | .tl-error-boundary__content button { 1569 | background: none; 1570 | border: none; 1571 | font-family: inherit; 1572 | font-size: 14px; 1573 | font-weight: 500; 1574 | padding: var(--space-4); 1575 | border-radius: var(--radius-3); 1576 | cursor: var(--tl-cursor-pointer); 1577 | color: inherit; 1578 | background-color: transparent; 1579 | } 1580 | .tl-error-boundary__content button:hover { 1581 | background-color: var(--color-low); 1582 | } 1583 | 1584 | .tl-error-boundary__content a { 1585 | color: var(--color-text-1); 1586 | font-weight: 500; 1587 | text-decoration: none; 1588 | } 1589 | .tl-error-boundary__content a:hover { 1590 | color: var(--color-text-1); 1591 | } 1592 | 1593 | .tl-error-boundary__content__error { 1594 | position: relative; 1595 | } 1596 | .tl-error-boundary__content__error button { 1597 | position: absolute; 1598 | top: var(--space-2); 1599 | right: var(--space-2); 1600 | font-size: 12px; 1601 | padding: var(--space-2) var(--space-3); 1602 | background-color: var(--color-panel); 1603 | border-radius: var(--radius-1); 1604 | } 1605 | 1606 | .tl-error-boundary__content__actions { 1607 | display: flex; 1608 | justify-content: space-between; 1609 | gap: var(--space-4); 1610 | margin: 0px; 1611 | margin-left: -4px; 1612 | } 1613 | .tl-error-boundary__content__actions__group { 1614 | display: flex; 1615 | gap: var(--space-4); 1616 | } 1617 | .tl-error-boundary__content .tl-error-boundary__reset { 1618 | color: var(--color-warn); 1619 | } 1620 | .tl-error-boundary__content .tl-error-boundary__refresh { 1621 | background-color: var(--color-primary); 1622 | color: var(--color-selected-contrast); 1623 | } 1624 | .tl-error-boundary__content .tl-error-boundary__refresh:hover { 1625 | background-color: var(--color-primary); 1626 | opacity: 0.9; 1627 | } 1628 | 1629 | /* --------------------- Coarse --------------------- */ 1630 | 1631 | @media screen and (pointer: coarse) { 1632 | /* If mobile always show handle-hint as there is no hover state */ 1633 | .tl-canvas__mobile .tl-handle__hint { 1634 | opacity: 1; 1635 | } 1636 | } 1637 | 1638 | .tl-hidden { 1639 | opacity: 0; 1640 | pointer-events: none; 1641 | } 1642 | 1643 | .debug__ui-logger { 1644 | position: absolute; 1645 | top: 62px; 1646 | left: 16px; 1647 | color: #555; 1648 | font-size: 12px; 1649 | font-family: monospace; 1650 | } 1651 | 1652 | /* @tldraw/ui */ 1653 | 1654 | .tl-container { 1655 | --layer-panels: 300; 1656 | --layer-menus: 400; 1657 | --layer-overlays: 500; 1658 | --layer-toasts: 650; 1659 | --layer-cursor: 700; 1660 | } 1661 | 1662 | /* -------------------------------------------------- */ 1663 | /* UI Refresh */ 1664 | /* -------------------------------------------------- */ 1665 | 1666 | /* Button */ 1667 | 1668 | .tlui-button { 1669 | position: relative; 1670 | height: 40px; 1671 | min-width: 40px; 1672 | padding: 0px 12px; 1673 | display: flex; 1674 | align-items: center; 1675 | justify-content: center; 1676 | background-color: transparent; 1677 | border: transparent; 1678 | color: currentColor; 1679 | cursor: pointer; 1680 | pointer-events: all; 1681 | font-weight: inherit; 1682 | font-family: inherit; 1683 | text-rendering: optimizeLegibility; 1684 | font-size: 12px; 1685 | gap: 0px; 1686 | color: var(--color-text-1); 1687 | text-shadow: 1px 1px var(--color-text-shadow); 1688 | } 1689 | 1690 | .tlui-button:disabled { 1691 | color: var(--color-text-3); 1692 | text-shadow: none; 1693 | } 1694 | 1695 | .tlui-button:disabled .tlui-kbd { 1696 | color: var(--color-text-3); 1697 | } 1698 | 1699 | .tlui-button > * { 1700 | position: relative; 1701 | z-index: 1; 1702 | } 1703 | 1704 | .tlui-button__label { 1705 | flex-grow: 2; 1706 | text-align: left; 1707 | } 1708 | 1709 | .tlui-button:focus-visible:not(:hover) { 1710 | outline: 1px solid var(--color-selected); 1711 | outline-offset: -4px; 1712 | border-radius: var(--radius-3); 1713 | } 1714 | 1715 | .tlui-button::after { 1716 | display: block; 1717 | content: ''; 1718 | position: absolute; 1719 | inset: 4px; 1720 | background-color: transparent; 1721 | border-radius: var(--radius-2); 1722 | } 1723 | 1724 | .tlui-button[aria-expanded='true']::after { 1725 | background-color: var(--color-muted-0); 1726 | opacity: 1; 1727 | } 1728 | 1729 | @media (hover: hover) { 1730 | .tlui-button::after { 1731 | background-color: var(--color-muted-2); 1732 | opacity: 0; 1733 | } 1734 | 1735 | .tlui-button:not(:disabled):hover::after { 1736 | opacity: 1; 1737 | } 1738 | } 1739 | 1740 | /* Low button */ 1741 | 1742 | .tlui-button__low { 1743 | border-radius: var(--radius-3); 1744 | background-color: var(--color-low); 1745 | } 1746 | 1747 | @media (hover: hover) { 1748 | .tlui-button__low::after { 1749 | background-color: var(--color-muted-2); 1750 | } 1751 | } 1752 | 1753 | /* Primary / danger buttons */ 1754 | 1755 | .tlui-button__primary { 1756 | color: var(--color-primary); 1757 | } 1758 | 1759 | .tlui-button__danger { 1760 | color: var(--color-warn); 1761 | text-shadow: none; 1762 | } 1763 | 1764 | @media (hover: hover) { 1765 | .tlui-button__primary:not(:disabled, :focus-visible):hover { 1766 | color: var(--color-primary); 1767 | } 1768 | 1769 | .tlui-button__danger:not(:disabled, :focus-visible):hover { 1770 | color: var(--color-warn); 1771 | text-shadow: none; 1772 | } 1773 | } 1774 | 1775 | /* Panel button */ 1776 | 1777 | .tlui-button__panel { 1778 | position: relative; 1779 | } 1780 | 1781 | /* Icon button */ 1782 | 1783 | .tlui-button__icon { 1784 | height: 40px; 1785 | width: 40px; 1786 | min-height: 40px; 1787 | min-width: 40px; 1788 | padding: 0px; 1789 | } 1790 | 1791 | /* Hinted */ 1792 | 1793 | .tlui-button__icon[data-state='hinted']::after { 1794 | background: var(--color-hint); 1795 | opacity: 1; 1796 | /* box-shadow: inset 0 0 0 1px var(--color-muted-1); */ 1797 | } 1798 | 1799 | .tlui-button__icon[data-state='hinted']:not(:disabled, :focus-visible):active::after { 1800 | background: var(--color-hint); 1801 | opacity: 1; 1802 | /* box-shadow: inset 0 0 0 1px var(--color-text-3); */ 1803 | } 1804 | 1805 | @media (hover: hover) { 1806 | .tlui-button__icon::after { 1807 | inset: 4px; 1808 | border-radius: var(--radius-2); 1809 | } 1810 | 1811 | .tlui-button__icon[data-state='hinted']:not(:disabled, :focus-visible):hover::after { 1812 | background: var(--color-hint); 1813 | /* box-shadow: inset 0 0 0 1px var(--color-text-3); */ 1814 | } 1815 | } 1816 | 1817 | /* Menu button */ 1818 | 1819 | .tlui-button__menu { 1820 | height: 40px; 1821 | min-height: 40px; 1822 | min-width: 128px; 1823 | width: 100%; 1824 | gap: 8px; 1825 | margin: -4px 0px; 1826 | } 1827 | 1828 | .tlui-button__menu:nth-child(1) { 1829 | margin-top: 0px; 1830 | } 1831 | 1832 | .tlui-button__menu:nth-last-child(1) { 1833 | margin-bottom: 0px; 1834 | } 1835 | 1836 | @media (hover: hover) { 1837 | .tlui-button__menu::after { 1838 | inset: 4px; 1839 | border-radius: var(--radius-2); 1840 | } 1841 | } 1842 | 1843 | /* Menu checkbox button */ 1844 | 1845 | .tlui-button__checkbox { 1846 | padding-left: 8px; 1847 | } 1848 | 1849 | .tlui-button__checkbox__indicator { 1850 | width: 15px; 1851 | height: 15px; 1852 | } 1853 | 1854 | /* Tool lock button */ 1855 | 1856 | .tlui-toolbar__lock-button { 1857 | position: absolute; 1858 | top: 4px; 1859 | right: 0px; 1860 | pointer-events: all; 1861 | height: 40px; 1862 | width: 40px; 1863 | min-width: 0px; 1864 | border-radius: var(--radius-2); 1865 | } 1866 | 1867 | .tlui-toolbar__lock-button::after { 1868 | top: 4px; 1869 | left: 8px; 1870 | inset: 4px; 1871 | } 1872 | 1873 | /* Tool button */ 1874 | 1875 | .tlui-button__tool { 1876 | position: relative; 1877 | height: 48px; 1878 | width: 48px; 1879 | margin-left: -2px; 1880 | margin-right: -2px; 1881 | } 1882 | 1883 | .tlui-button__tool:nth-of-type(1) { 1884 | margin-left: 0px; 1885 | } 1886 | 1887 | .tlui-button__tool:nth-last-of-type(1) { 1888 | margin-right: 0px; 1889 | } 1890 | 1891 | @media (hover: hover) { 1892 | .tlui-button__tool::after { 1893 | inset: 4px; 1894 | border-radius: 8px; 1895 | } 1896 | 1897 | .tlui-button__tool[data-state='selected']:not(:disabled, :focus-visible):hover { 1898 | color: var(--color-selected-contrast); 1899 | } 1900 | } 1901 | 1902 | .tlui-button__tool[data-state='selected'] { 1903 | color: var(--color-selected-contrast); 1904 | } 1905 | 1906 | .tlui-button__tool[data-state='selected']:not(:disabled, :focus-visible):active { 1907 | color: var(--color-selected-contrast); 1908 | } 1909 | 1910 | .tlui-button__tool[data-state='selected']:not(:disabled)::after { 1911 | background: var(--color-selected); 1912 | opacity: 1; 1913 | } 1914 | 1915 | /* Help */ 1916 | 1917 | .tlui-button__help { 1918 | height: 32px; 1919 | width: 32px; 1920 | padding: 0px; 1921 | min-width: 32px; 1922 | border-radius: 100%; 1923 | background-color: var(--color-low); 1924 | border: 1px solid var(--color-low-border); 1925 | } 1926 | 1927 | @media (hover: hover) { 1928 | .tlui-button__help::after { 1929 | background-color: var(--color-muted-2); 1930 | border-radius: 100%; 1931 | inset: 4px; 1932 | } 1933 | } 1934 | 1935 | /* Button Row */ 1936 | 1937 | .tlui-buttons__horizontal { 1938 | display: flex; 1939 | flex-direction: row; 1940 | } 1941 | .tlui-buttons__horizontal > * { 1942 | margin-left: -2px; 1943 | margin-right: -2px; 1944 | } 1945 | .tlui-buttons__horizontal > *:nth-child(1) { 1946 | margin-left: 0px; 1947 | } 1948 | .tlui-buttons__horizontal > *:nth-last-child(1) { 1949 | margin-right: 0px; 1950 | } 1951 | 1952 | /* Button Grid */ 1953 | 1954 | .tlui-buttons__grid { 1955 | display: grid; 1956 | grid-template-columns: repeat(4, auto); 1957 | grid-auto-flow: row; 1958 | overflow: hidden; 1959 | } 1960 | .tlui-buttons__grid > .tlui-button { 1961 | margin: -2px; 1962 | } 1963 | .tlui-buttons__grid > .tlui-button:nth-of-type(4n) { 1964 | margin-right: 0px; 1965 | } 1966 | .tlui-buttons__grid > .tlui-button:nth-of-type(4n - 3) { 1967 | margin-left: 0px; 1968 | } 1969 | .tlui-buttons__grid > .tlui-button:nth-of-type(-n + 4) { 1970 | margin-top: 0px; 1971 | } 1972 | .tlui-buttons__grid > .tlui-button:nth-last-of-type(-n + 4) { 1973 | margin-bottom: 0px; 1974 | } 1975 | 1976 | /* Zoom button */ 1977 | 1978 | .tlui-zoom-menu__button__pct { 1979 | width: 60px; 1980 | min-width: 60px; 1981 | text-align: center; 1982 | } 1983 | 1984 | @media (max-width: 640px) { 1985 | .tlui-button__tool { 1986 | height: 48px; 1987 | width: 44px; 1988 | } 1989 | 1990 | .tlui-button__tool > .tlui-icon { 1991 | height: 16px; 1992 | width: 16px; 1993 | } 1994 | 1995 | .tlui-kbd { 1996 | visibility: hidden; 1997 | } 1998 | 1999 | .tlui-menu__group .tlui-button__icon-left { 2000 | display: none; 2001 | } 2002 | } 2003 | 2004 | /* --------------------- Layout --------------------- */ 2005 | 2006 | .tlui-layout { 2007 | position: relative; 2008 | display: grid; 2009 | grid-template-columns: 1fr; 2010 | grid-template-rows: minmax(0px, 1fr) auto; 2011 | grid-auto-rows: auto; 2012 | height: 100%; 2013 | max-height: 100%; 2014 | overflow: clip; 2015 | pointer-events: none; 2016 | user-select: none; 2017 | contain: strict; 2018 | z-index: var(--layer-panels); 2019 | -webkit-transform: translate3d(0, 0, 0); 2020 | --sab: env(safe-area-inset-bottom); 2021 | } 2022 | 2023 | .tlui-layout__top { 2024 | grid-column: 1; 2025 | grid-row: 1; 2026 | display: flex; 2027 | min-width: 0px; 2028 | justify-content: space-between; 2029 | } 2030 | 2031 | .tlui-layout__top__left { 2032 | display: flex; 2033 | flex-direction: column; 2034 | align-items: flex-start; 2035 | justify-content: flex-start; 2036 | width: 100%; 2037 | height: 100%; 2038 | flex: 0 1 0; 2039 | } 2040 | 2041 | .tlui-layout__top__right { 2042 | display: flex; 2043 | flex-direction: column; 2044 | align-items: flex-end; 2045 | justify-content: flex-start; 2046 | height: 100%; 2047 | flex: 0 0 auto; 2048 | min-width: 0px; 2049 | } 2050 | 2051 | .scrollable, 2052 | .scrollable * { 2053 | pointer-events: all; 2054 | touch-action: auto; 2055 | overscroll-behavior: none; 2056 | } 2057 | 2058 | /* ----------------- Helper Buttons ---------------- */ 2059 | 2060 | .tlui-helper-buttons { 2061 | position: relative; 2062 | display: flex; 2063 | flex-direction: column; 2064 | justify-content: flex-start; 2065 | align-items: flex-start; 2066 | width: min-content; 2067 | gap: var(--space-3); 2068 | margin: var(--space-2) var(--space-3); 2069 | white-space: nowrap; 2070 | pointer-events: none; 2071 | z-index: var(--layer-panels); 2072 | } 2073 | 2074 | /* ---------------------- Icon ---------------------- */ 2075 | 2076 | .tlui-icon { 2077 | flex-shrink: 0; 2078 | width: 18px; 2079 | height: 18px; 2080 | background-color: currentColor; 2081 | } 2082 | 2083 | .tlui-icon__small { 2084 | width: 15px; 2085 | height: 15px; 2086 | } 2087 | 2088 | /* --------------------- Slider --------------------- */ 2089 | 2090 | .tlui-slider { 2091 | position: relative; 2092 | display: flex; 2093 | align-items: center; 2094 | user-select: none; 2095 | touch-action: none; 2096 | } 2097 | 2098 | .tlui-slider__container { 2099 | width: 100%; 2100 | padding: 0px var(--space-4); 2101 | } 2102 | 2103 | .tlui-slider__track { 2104 | position: relative; 2105 | flex-grow: 1; 2106 | height: 44px; 2107 | cursor: pointer; 2108 | } 2109 | 2110 | .tlui-slider__track::after { 2111 | display: block; 2112 | position: absolute; 2113 | top: calc(50% - 2px); 2114 | content: ''; 2115 | height: 3px; 2116 | width: 100%; 2117 | background-color: var(--color-muted-1); 2118 | border-radius: 14px; 2119 | } 2120 | 2121 | .tlui-slider__range { 2122 | position: absolute; 2123 | top: calc(50% - 2px); 2124 | left: 0px; 2125 | height: 3px; 2126 | background-color: var(--color-selected); 2127 | border-radius: 14px; 2128 | } 2129 | 2130 | .tlui-slider__thumb { 2131 | all: unset; 2132 | cursor: grab; 2133 | display: block; 2134 | width: 18px; 2135 | height: 18px; 2136 | position: relative; 2137 | top: -1px; 2138 | background-color: var(--color-panel); 2139 | border-radius: 999px; 2140 | box-shadow: inset 0px 0px 0px 2px var(--color-text-1); 2141 | } 2142 | 2143 | .tlui-slider__thumb:active { 2144 | cursor: grabbing; 2145 | box-shadow: inset 0px 0px 0px 2px var(--color-text-1), var(--shadow-1); 2146 | } 2147 | 2148 | .tlui-slider__thumb:focus-visible { 2149 | box-shadow: inset 0 0 0 2px var(--color-focus); 2150 | } 2151 | 2152 | /* ----------------------- Kbd ---------------------- */ 2153 | 2154 | .tlui-kbd { 2155 | font-family: inherit; 2156 | font-size: 11px; 2157 | line-height: 11px; 2158 | display: grid; 2159 | justify-items: center; 2160 | grid-auto-flow: column; 2161 | grid-template-columns: auto; 2162 | grid-auto-columns: minmax(1em, auto); 2163 | gap: 1px; 2164 | align-self: bottom; 2165 | color: var(--color-text-1); 2166 | margin-left: var(--space-4); 2167 | } 2168 | 2169 | .tlui-kbd > span { 2170 | width: 100%; 2171 | text-align: center; 2172 | display: inline; 2173 | margin: 0px; 2174 | padding: 2px; 2175 | border-radius: 2px; 2176 | } 2177 | 2178 | .tlui-kbd:not(:last-child) { 2179 | margin-right: var(--space-2); 2180 | } 2181 | 2182 | /* Focus Mode Button */ 2183 | 2184 | .tlui-focus-button { 2185 | z-index: var(--layer-panels); 2186 | pointer-events: all; 2187 | } 2188 | 2189 | /* --------------------- Popover -------------------- */ 2190 | 2191 | .tlui-popover { 2192 | position: relative; 2193 | display: flex; 2194 | align-content: stretch; 2195 | } 2196 | 2197 | .tlui-popover__content { 2198 | position: relative; 2199 | max-height: 75vh; 2200 | margin: 0px; 2201 | border: none; 2202 | border-radius: var(--radius-3); 2203 | background-color: var(--color-panel); 2204 | box-shadow: var(--shadow-3); 2205 | z-index: var(--layer-menus); 2206 | overflow: hidden; 2207 | overflow-y: auto; 2208 | touch-action: auto; 2209 | overscroll-behavior: none; 2210 | scrollbar-width: none; 2211 | -ms-overflow-style: none; 2212 | } 2213 | 2214 | /* -------------------------------------------------- */ 2215 | /* Zones */ 2216 | /* -------------------------------------------------- */ 2217 | 2218 | /* ------------------- Status Bar ------------------- */ 2219 | 2220 | .tlui-debug-panel { 2221 | background-color: var(--color-low); 2222 | width: 100%; 2223 | display: grid; 2224 | align-items: center; 2225 | grid-template-columns: 1fr auto auto; 2226 | justify-content: space-between; 2227 | padding-left: var(--space-4); 2228 | border-top: 1px solid var(--color-background); 2229 | font-size: 12px; 2230 | color: var(--color-text-1); 2231 | z-index: var(--layer-panels); 2232 | pointer-events: all; 2233 | } 2234 | 2235 | .tlui-debug-panel__current-state { 2236 | white-space: nowrap; 2237 | } 2238 | 2239 | /* -------------------- Menu Zone ------------------- */ 2240 | 2241 | .tlui-menu-zone { 2242 | position: relative; 2243 | z-index: var(--layer-panels); 2244 | width: fit-content; 2245 | border-right: 4px solid var(--color-background); 2246 | border-bottom: 4px solid var(--color-background); 2247 | border-bottom-right-radius: var(--radius-4); 2248 | background-color: var(--color-low); 2249 | } 2250 | 2251 | .tlui-menu-zone *[data-state='open']::after { 2252 | background: linear-gradient(180deg, rgba(144, 144, 144, 0) 0%, var(--color-muted-2) 100%); 2253 | } 2254 | 2255 | /* ------------------- Style Panel ------------------ */ 2256 | 2257 | .tlui-style-panel__wrapper { 2258 | box-shadow: var(--shadow-2); 2259 | border-radius: var(--radius-3); 2260 | pointer-events: all; 2261 | background-color: var(--color-panel); 2262 | height: fit-content; 2263 | max-height: 100%; 2264 | margin: 4px 8px; 2265 | touch-action: auto; 2266 | overscroll-behavior: none; 2267 | overflow-y: auto; 2268 | overflow-x: hidden; 2269 | color: var(--color-text); 2270 | } 2271 | 2272 | .tlui-style-panel { 2273 | position: relative; 2274 | z-index: var(--layer-panels); 2275 | pointer-events: all; 2276 | width: 148px; 2277 | max-width: 148px; 2278 | } 2279 | 2280 | .tlui-style-panel::-webkit-scrollbar { 2281 | display: none; 2282 | } 2283 | 2284 | .tlui-style-panel .tlui-button.select { 2285 | width: 100%; 2286 | } 2287 | 2288 | .tlui-style-panel__section { 2289 | display: flex; 2290 | position: relative; 2291 | flex-direction: column; 2292 | } 2293 | 2294 | .tlui-style-panel__section:nth-of-type(n + 2):not(:last-child) { 2295 | border-bottom: 1px solid var(--color-divider); 2296 | } 2297 | 2298 | .tlui-style-panel__section:empty { 2299 | display: none; 2300 | } 2301 | 2302 | .tlui-style-panel__section__common:not(:only-child) { 2303 | margin-bottom: 7px; 2304 | border-bottom: 1px solid var(--color-divider); 2305 | } 2306 | 2307 | .tlui-style-panel__row { 2308 | display: flex; 2309 | } 2310 | /* Only really used for the alignment picker */ 2311 | .tlui-style-panel__row__extra-button { 2312 | margin-left: -2px; 2313 | } 2314 | 2315 | .tlui-style-panel__double-select-picker__wrapper { 2316 | width: 100%; 2317 | max-width: 100%; 2318 | } 2319 | 2320 | .tlui-style-panel__double-select-picker { 2321 | display: flex; 2322 | grid-template-columns: 1fr auto; 2323 | align-items: center; 2324 | padding-left: var(--space-4); 2325 | color: var(--color-text-1); 2326 | font-size: 12px; 2327 | } 2328 | 2329 | .tlui-style-panel__double-select-picker-label { 2330 | text-overflow: ellipsis; 2331 | overflow: hidden; 2332 | white-space: nowrap; 2333 | flex-grow: 2; 2334 | max-width: 100%; 2335 | } 2336 | 2337 | .tlui-style-panel__section *[data-state='open']::after { 2338 | background: var(--color-muted-0); 2339 | } 2340 | 2341 | /* ---------------------- Input --------------------- */ 2342 | 2343 | .tlui-input { 2344 | background: none; 2345 | margin: 0px; 2346 | position: relative; 2347 | z-index: 1; 2348 | height: 40px; 2349 | max-height: 40px; 2350 | display: flex; 2351 | align-items: center; 2352 | justify-content: center; 2353 | font-family: inherit; 2354 | font-size: 12px; 2355 | font-weight: inherit; 2356 | color: var(--color-text-1); 2357 | padding: var(--space-4); 2358 | padding-left: 0px; 2359 | border: none; 2360 | outline: none; 2361 | text-overflow: ellipsis; 2362 | width: 100%; 2363 | user-select: all; 2364 | text-rendering: optimizeLegibility; 2365 | -webkit-user-select: auto !important; 2366 | } 2367 | 2368 | .tlui-input__wrapper { 2369 | width: 100%; 2370 | height: 44px; 2371 | display: flex; 2372 | align-items: center; 2373 | gap: var(--space-4); 2374 | color: var(--color-text); 2375 | } 2376 | 2377 | .tlui-input__wrapper > .tlui-icon { 2378 | flex-shrink: 0; 2379 | } 2380 | 2381 | /* If mobile use 16px as font size */ 2382 | /* On iOS, font size under 16px in an input will make the page zoom into the input 🤦♂️ */ 2383 | /* https://css-tricks.com/16px-or-larger-text-prevents-ios-form-zoom/ */ 2384 | @media (max-width: 600px) { 2385 | @supports (-webkit-touch-callout: none) { 2386 | /* CSS specific to iOS devices */ 2387 | .tlui-input { 2388 | font-size: 16px; 2389 | } 2390 | } 2391 | } 2392 | 2393 | /* ---------------- Prompt ---------------- */ 2394 | 2395 | .tlui-prompt__overlay { 2396 | background: var(--color-overlay); 2397 | position: fixed; 2398 | top: 0px; 2399 | left: 0px; 2400 | right: 0px; 2401 | bottom: 0px; 2402 | display: grid; 2403 | place-items: center; 2404 | overflow-y: auto; 2405 | z-index: var(--layer-overlays); 2406 | } 2407 | 2408 | .tlui-prompt__content { 2409 | cursor: default; 2410 | background-color: var(--color-panel); 2411 | box-shadow: var(--shadow-3); 2412 | border-radius: var(--radius-4); 2413 | padding: var(--space-5); 2414 | font-size: 12px; 2415 | overflow-y: auto; 2416 | min-width: 300px; 2417 | max-width: 80vw; 2418 | max-height: 80vh; 2419 | } 2420 | 2421 | .tlui-prompt__actions { 2422 | border: none; 2423 | padding: 0px; 2424 | margin: 0px; 2425 | display: flex; 2426 | justify-content: flex-end; 2427 | margin-right: calc(-1 * var(--space-3)); 2428 | margin-bottom: calc(-1 * var(--space-3)); 2429 | } 2430 | 2431 | .tlui-prompt__title { 2432 | margin: 0px; 2433 | font-size: 12px; 2434 | } 2435 | 2436 | .tlui-prompt__error { 2437 | color: #d10b0b; 2438 | display: flex; 2439 | gap: var(--space-2); 2440 | align-items: center; 2441 | } 2442 | 2443 | /* ---------------- Dialog ---------------- */ 2444 | 2445 | .tlui-dialog__overlay { 2446 | position: absolute; 2447 | top: 0px; 2448 | left: 0px; 2449 | width: 100%; 2450 | height: 100%; 2451 | z-index: var(--layer-overlays); 2452 | background-color: var(--color-overlay); 2453 | pointer-events: all; 2454 | animation: fadeIn 0.12s ease-out; 2455 | display: grid; 2456 | place-items: center; 2457 | overflow-y: auto; 2458 | } 2459 | 2460 | .tlui-dialog__content { 2461 | display: flex; 2462 | flex-direction: column; 2463 | position: relative; 2464 | cursor: default; 2465 | background-color: var(--color-panel); 2466 | box-shadow: var(--shadow-3); 2467 | border-radius: var(--radius-3); 2468 | font-size: 12px; 2469 | overflow: hidden; 2470 | min-width: 300px; 2471 | max-width: 80vw; 2472 | max-height: 80vh; 2473 | } 2474 | 2475 | .tlui-dialog__header { 2476 | position: relative; 2477 | display: flex; 2478 | align-items: center; 2479 | flex: 0; 2480 | z-index: 999; 2481 | padding-left: var(--space-4); 2482 | color: var(--color-text); 2483 | height: 44px; 2484 | } 2485 | 2486 | .tlui-dialog__header__title { 2487 | flex: 1; 2488 | font-weight: inherit; 2489 | font-size: 12px; 2490 | margin: 0px; 2491 | color: var(--color-text-1); 2492 | } 2493 | 2494 | .tlui-dialog__header__close { 2495 | justify-self: flex-end; 2496 | } 2497 | 2498 | .tlui-dialog__body { 2499 | padding: var(--space-4) var(--space-4); 2500 | flex: 0 1; 2501 | overflow-y: auto; 2502 | overflow-x: hidden; 2503 | color: var(--color-text-1); 2504 | user-select: all; 2505 | -webkit-user-select: text; 2506 | } 2507 | 2508 | .tlui-dialog__footer { 2509 | position: relative; 2510 | z-index: 999; 2511 | } 2512 | 2513 | .tlui-dialog__footer__actions { 2514 | display: flex; 2515 | align-items: center; 2516 | justify-content: flex-end; 2517 | } 2518 | 2519 | .tlui-dialog__footer__actions > .tlui-button:nth-last-child(n + 2) { 2520 | margin-right: -4px; 2521 | } 2522 | 2523 | /* --------------------- Toolbar -------------------- */ 2524 | 2525 | /* Wide container */ 2526 | .tlui-toolbar { 2527 | grid-column: 1 / span 3; 2528 | grid-row: 1; 2529 | display: flex; 2530 | align-items: center; 2531 | justify-content: center; 2532 | flex-grow: 2; 2533 | padding-bottom: calc(var(--space-3) + var(--sab)); 2534 | } 2535 | 2536 | /* Centered Content */ 2537 | .tlui-toolbar__inner { 2538 | position: relative; 2539 | width: fit-content; 2540 | display: flex; 2541 | gap: var(--space-3); 2542 | align-items: flex-end; 2543 | } 2544 | 2545 | .tlui-toolbar__left { 2546 | width: fit-content; 2547 | } 2548 | 2549 | /* Row of controls + lock button */ 2550 | .tlui-toolbar__extras { 2551 | position: relative; 2552 | z-index: 1; 2553 | width: 100%; 2554 | pointer-events: none; 2555 | top: 4px; 2556 | } 2557 | 2558 | .tlui-toolbar__extras__controls { 2559 | display: flex; 2560 | position: relative; 2561 | flex-direction: row; 2562 | z-index: 1; 2563 | background-color: var(--color-low); 2564 | border-top-left-radius: var(--radius-4); 2565 | border-top-right-radius: var(--radius-4); 2566 | border: 4px solid var(--color-background); 2567 | margin-left: 8px; 2568 | margin-right: 0px; 2569 | pointer-events: all; 2570 | width: fit-content; 2571 | } 2572 | 2573 | .tlui-toolbar__tools { 2574 | display: flex; 2575 | flex-direction: row; 2576 | background-color: var(--color-low); 2577 | border-radius: 11px; 2578 | z-index: var(--layer-panels); 2579 | pointer-events: all; 2580 | position: relative; 2581 | align-items: center; 2582 | background: var(--color-panel); 2583 | box-shadow: var(--shadow-2); 2584 | } 2585 | 2586 | .tlui-toolbar__overflow { 2587 | width: 40px; 2588 | } 2589 | 2590 | @media (max-width: 640px) { 2591 | .tlui-toolbar__overflow { 2592 | width: 32px; 2593 | padding: 0px; 2594 | } 2595 | 2596 | .tlui-toolbar *[data-state='open']::after { 2597 | background: linear-gradient(0deg, rgba(144, 144, 144, 0) 0%, var(--color-muted-2) 100%); 2598 | } 2599 | } 2600 | 2601 | /* -------------------- Help Zone ------------------- */ 2602 | 2603 | .tlui-help-menu { 2604 | pointer-events: all; 2605 | position: absolute; 2606 | bottom: var(--space-2); 2607 | right: var(--space-2); 2608 | z-index: var(--layer-panels); 2609 | border: 4px solid var(--color-background); 2610 | border-radius: 100%; 2611 | } 2612 | 2613 | /* ------------------ Context Menu ------------------ */ 2614 | 2615 | .tlui-context-menu__move-to-page__name { 2616 | max-width: calc(min(300px, 35vw)); 2617 | overflow: hidden; 2618 | text-overflow: ellipsis; 2619 | } 2620 | 2621 | .tlui-context-menu__move-to-page__name[data-disabled] { 2622 | color: var(--color-text-3); 2623 | pointer-events: none; 2624 | } 2625 | 2626 | /* ---------------------- Menu ---------------------- */ 2627 | 2628 | .tlui-menu { 2629 | z-index: var(--layer-menus); 2630 | height: fit-content; 2631 | width: fit-content; 2632 | max-height: 80vh; 2633 | border-radius: var(--radius-3); 2634 | pointer-events: all; 2635 | touch-action: auto; 2636 | overflow-y: auto; 2637 | overscroll-behavior: none; 2638 | background-color: var(--color-panel); 2639 | box-shadow: var(--shadow-3); 2640 | } 2641 | 2642 | .tlui-menu::-webkit-scrollbar { 2643 | display: none; 2644 | } 2645 | 2646 | .tlui-menu__arrow { 2647 | position: relative; 2648 | top: -1px; 2649 | fill: var(--color-panel); 2650 | stroke: var(--color-panel-contrast); 2651 | stroke-width: 1px; 2652 | } 2653 | 2654 | .tlui-menu__group[data-size='large'] { 2655 | min-width: initial; 2656 | } 2657 | 2658 | .tlui-menu__group[data-size='medium'] { 2659 | min-width: 144px; 2660 | } 2661 | 2662 | .tlui-menu__group[data-size='small'] { 2663 | min-width: 96px; 2664 | } 2665 | 2666 | .tlui-menu__group[data-size='tiny'] { 2667 | min-width: 0px; 2668 | } 2669 | 2670 | .tlui-menu__group + .tlui-menu__group { 2671 | border-top: 1px solid var(--color-divider); 2672 | } 2673 | 2674 | .tlui-menu__submenu__trigger[data-state='open']:not(:hover)::after { 2675 | border-radius: var(--radius-1); 2676 | background: linear-gradient(90deg, rgba(144, 144, 144, 0) 0%, var(--color-muted-2) 100%); 2677 | } 2678 | 2679 | .tlui-menu__submenu__trigger[data-direction='left'][data-state='open']:not(:hover)::after { 2680 | border-radius: var(--radius-1); 2681 | background: linear-gradient(270deg, rgba(144, 144, 144, 0) 0%, var(--color-muted-2) 100%); 2682 | } 2683 | 2684 | /* ------------------ Actions Menu ------------------ */ 2685 | 2686 | .tlui-actions-menu { 2687 | max-height: calc(100vh - 150px); 2688 | } 2689 | 2690 | /* --------------------- Toasts --------------------- */ 2691 | 2692 | .tlui-toast__viewport { 2693 | position: absolute; 2694 | inset: 0px; 2695 | margin: 0px; 2696 | display: flex; 2697 | align-items: flex-end; 2698 | justify-content: flex-end; 2699 | flex-direction: column; 2700 | gap: var(--space-3); 2701 | pointer-events: none; 2702 | padding: 0px var(--space-3) 64px 0px; 2703 | z-index: var(--layer-toasts); 2704 | } 2705 | 2706 | .tlui-toast__viewport > * { 2707 | pointer-events: all; 2708 | } 2709 | 2710 | .tlui-toast__icon { 2711 | padding-top: var(--space-4); 2712 | padding-left: var(--space-4); 2713 | color: var(--color-text-1); 2714 | } 2715 | 2716 | .tlui-toast__container { 2717 | min-width: 200px; 2718 | display: flex; 2719 | flex-direction: row; 2720 | gap: var(--space-3); 2721 | background-color: var(--color-panel); 2722 | box-shadow: var(--shadow-2); 2723 | border-radius: var(--radius-3); 2724 | font-size: 12px; 2725 | } 2726 | 2727 | .tlui-toast__main { 2728 | flex-grow: 2; 2729 | max-width: 280px; 2730 | } 2731 | 2732 | .tlui-toast__main:nth-child(1) > .tlui-toast__content { 2733 | padding-left: var(--space-4); 2734 | } 2735 | 2736 | .tlui-toast__content { 2737 | padding-left: 0px; 2738 | padding-top: var(--space-4); 2739 | padding-bottom: var(--space-4); 2740 | display: flex; 2741 | flex-direction: column; 2742 | gap: var(--space-3); 2743 | } 2744 | 2745 | .tlui-toast__content:not(:only-child) { 2746 | padding-bottom: var(--space-2); 2747 | } 2748 | 2749 | .tlui-toast__title { 2750 | font-weight: bold; 2751 | color: var(--color-text-1); 2752 | } 2753 | 2754 | .tlui-toast__description { 2755 | color: var(--color-text-1); 2756 | padding: var(--space-3); 2757 | margin: 0px; 2758 | padding: 0px; 2759 | } 2760 | 2761 | .tlui-toast__icon + .tlui-toast__main > .tlui-toast__actions { 2762 | margin-left: -12px; 2763 | padding-left: 0px; 2764 | } 2765 | 2766 | .tlui-toast__actions { 2767 | display: flex; 2768 | flex-direction: row; 2769 | justify-content: flex-start; 2770 | margin-left: 0; 2771 | } 2772 | 2773 | .tlui-toast__close { 2774 | align-self: flex-end; 2775 | flex-shrink: 0; 2776 | } 2777 | 2778 | @media (prefers-reduced-motion: no-preference) { 2779 | .tlui-toast__container[data-state='open'] { 2780 | animation: slide-in 200ms cubic-bezier(0.785, 0.135, 0.15, 0.86); 2781 | } 2782 | 2783 | .tlui-toast__container[data-state='closed'] { 2784 | animation: hide 100ms ease-in; 2785 | } 2786 | 2787 | .tlui-toast__container[data-swipe='move'] { 2788 | transform: translateX(var(--radix-toast-swipe-move-x)); 2789 | } 2790 | 2791 | .tlui-toast__container[data-swipe='cancel'] { 2792 | transform: translateX(0); 2793 | transition: transform 200ms ease-out; 2794 | } 2795 | 2796 | .tlui-toast__container[data-swipe='end'] { 2797 | animation: swipe-out 100ms ease-out; 2798 | } 2799 | } 2800 | 2801 | /* --------------------- Bottom --------------------- */ 2802 | 2803 | .tlui-layout__bottom { 2804 | grid-row: 2; 2805 | } 2806 | 2807 | .tlui-layout__bottom__main { 2808 | width: 100%; 2809 | position: relative; 2810 | display: flex; 2811 | align-items: flex-end; 2812 | justify-content: center; 2813 | } 2814 | 2815 | /* ------------------- Navigation ------------------- */ 2816 | 2817 | .tlui-navigation-zone { 2818 | display: flex; 2819 | width: min-content; 2820 | flex-direction: column; 2821 | z-index: var(--layer-panels); 2822 | pointer-events: all; 2823 | position: absolute; 2824 | left: 0px; 2825 | bottom: 0px; 2826 | } 2827 | 2828 | .tlui-navigation-zone::before { 2829 | content: ''; 2830 | display: block; 2831 | position: absolute; 2832 | z-index: -1; 2833 | inset: -4px -4px 0px 0px; 2834 | border-radius: 0; 2835 | border-top: 4px solid var(--color-background); 2836 | border-right: 4px solid var(--color-background); 2837 | border-top-right-radius: var(--radius-4); 2838 | background-color: var(--color-low); 2839 | } 2840 | 2841 | .tlui-navigation-zone__toggle .tlui-icon { 2842 | opacity: 0.24; 2843 | } 2844 | 2845 | .tlui-navigation-zone__toggle:active .tlui-icon { 2846 | opacity: 1; 2847 | } 2848 | 2849 | @media (hover: hover) { 2850 | .tlui-navigation-zone__toggle:hover .tlui-icon { 2851 | opacity: 1; 2852 | } 2853 | } 2854 | 2855 | .tlui-minimap { 2856 | width: 100%; 2857 | height: 96px; 2858 | min-height: 96px; 2859 | overflow: hidden; 2860 | padding: var(--space-3); 2861 | padding-top: 0px; 2862 | } 2863 | 2864 | .tlui-minimap__canvas { 2865 | position: relative; 2866 | width: 100%; 2867 | height: 100%; 2868 | } 2869 | 2870 | /* ----------------------- ... ---------------------- */ 2871 | 2872 | @keyframes hide { 2873 | 0% { 2874 | opacity: 1; 2875 | } 2876 | 100% { 2877 | opacity: 0; 2878 | } 2879 | } 2880 | 2881 | @keyframes slide-in { 2882 | from { 2883 | transform: translateX(calc(100% + var(--space-3))); 2884 | } 2885 | to { 2886 | transform: translateX(0px); 2887 | } 2888 | } 2889 | 2890 | @keyframes swipe-out { 2891 | from { 2892 | transform: translateX(var(--radix-toast-swipe-end-x)); 2893 | } 2894 | to { 2895 | transform: translateX(calc(100% + var(--space-3))); 2896 | } 2897 | } 2898 | 2899 | /* ------------------- Page Select ------------------ */ 2900 | 2901 | .tlui-page-menu__wrapper { 2902 | position: relative; 2903 | display: flex; 2904 | flex-direction: column; 2905 | width: 220px; 2906 | height: fit-content; 2907 | max-height: 50vh; 2908 | } 2909 | 2910 | .tlui-page-menu__trigger { 2911 | width: 128px; 2912 | } 2913 | 2914 | .tlui-page-menu__trigger > span { 2915 | flex-grow: 2; 2916 | margin-right: var(--space-4); 2917 | } 2918 | 2919 | .tlui-page-menu__header { 2920 | display: flex; 2921 | flex-direction: row; 2922 | align-items: center; 2923 | width: 100%; 2924 | padding-left: var(--space-4); 2925 | border-bottom: 1px solid var(--color-divider); 2926 | } 2927 | 2928 | .tlui-page-menu__header > .tlui-button:nth-of-type(1) { 2929 | margin-right: -4px; 2930 | } 2931 | 2932 | .tlui-page-menu__header__title { 2933 | color: var(--color-text); 2934 | font-size: 12px; 2935 | flex-grow: 2; 2936 | } 2937 | 2938 | .tlui-page-menu__name { 2939 | flex-grow: 2; 2940 | text-align: left; 2941 | overflow: hidden; 2942 | text-overflow: ellipsis; 2943 | white-space: nowrap; 2944 | } 2945 | 2946 | .tlui-page-menu__list { 2947 | position: relative; 2948 | touch-action: auto; 2949 | flex-direction: column; 2950 | max-height: 100%; 2951 | overflow-x: hidden; 2952 | overflow-y: auto; 2953 | touch-action: auto; 2954 | } 2955 | 2956 | .tlui-page-menu__item { 2957 | display: flex; 2958 | flex-direction: row; 2959 | align-items: center; 2960 | justify-content: space-between; 2961 | gap: 0px; 2962 | } 2963 | 2964 | .tlui-page-menu__item:nth-of-type(n + 2) { 2965 | margin-top: -4px; 2966 | } 2967 | 2968 | .tlui-page-menu__item__button:not(:only-child) { 2969 | flex-grow: 2; 2970 | margin-right: -2px; 2971 | } 2972 | 2973 | .tlui-page-menu__item__button > span { 2974 | display: block; 2975 | flex-grow: 2; 2976 | text-align: left; 2977 | overflow: hidden; 2978 | text-overflow: ellipsis; 2979 | white-space: nowrap; 2980 | } 2981 | 2982 | .tlui-page-menu__item__button__checkbox { 2983 | padding-left: 35px; 2984 | } 2985 | 2986 | .tlui-page-menu__item__button__check { 2987 | position: absolute; 2988 | left: 0px; 2989 | width: 24px; 2990 | padding-left: 10px; 2991 | display: inline-flex; 2992 | align-items: center; 2993 | justify-content: center; 2994 | color: var(--color-text); 2995 | } 2996 | 2997 | .tlui-page_menu__item__sortable { 2998 | position: absolute; 2999 | top: 0px; 3000 | left: 0px; 3001 | width: 100%; 3002 | height: fit-content; 3003 | display: flex; 3004 | flex-direction: row; 3005 | align-items: center; 3006 | overflow: hidden; 3007 | z-index: 1; 3008 | } 3009 | 3010 | .tlui-page_menu__item__sortable__title { 3011 | flex: 1; 3012 | } 3013 | 3014 | .tlui-page_menu__item__sortable__title > .tlui-input__wrapper { 3015 | height: 100%; 3016 | } 3017 | 3018 | .tlui-page_menu__item__sortable:focus-within { 3019 | z-index: 10; 3020 | } 3021 | 3022 | .tlui-page_menu__item__sortable__handle { 3023 | touch-action: none; 3024 | width: 32px; 3025 | min-width: 0px; 3026 | height: 40px; 3027 | cursor: grab; 3028 | color: var(--color-text-3); 3029 | flex-shrink: 0; 3030 | margin-right: -9px; 3031 | } 3032 | 3033 | .tlui-page_menu__item__sortable__handle:active { 3034 | cursor: grabbing; 3035 | } 3036 | 3037 | .tlui-page-menu__item__input { 3038 | margin-left: 12px; 3039 | height: 100%; 3040 | } 3041 | 3042 | /* The more menu has complex CSS here: */ 3043 | /* If the user can hover, then visible but opacity zero until hover */ 3044 | /* If the user cannot hover, then not displayed unless editing, and then opacity 1 */ 3045 | 3046 | .tlui-page_menu__item__submenu { 3047 | pointer-events: all; 3048 | flex: 0; 3049 | cursor: pointer; 3050 | margin: 0px; 3051 | display: none; 3052 | margin-left: -2px; 3053 | } 3054 | 3055 | .tlui-page_menu__item__submenu[data-isediting='true'] { 3056 | display: block; 3057 | } 3058 | 3059 | @media (hover: hover) { 3060 | .tlui-page_menu__item__submenu { 3061 | opacity: 0; 3062 | display: block; 3063 | } 3064 | 3065 | .tlui-page_menu__item__submenu:hover, 3066 | .tlui-page-menu__item:focus-within > .tlui-page_menu__item__submenu, 3067 | .tlui-page_menu__item__sortable:focus-within > .tlui-page_menu__item__submenu { 3068 | opacity: 1; 3069 | } 3070 | } 3071 | 3072 | /* --------------------- Dialogs -------------------- */ 3073 | 3074 | /* Edit Link Dialog */ 3075 | 3076 | .tlui-edit-link-dialog { 3077 | display: flex; 3078 | flex-direction: column; 3079 | gap: var(--space-4); 3080 | color: var(--color-text); 3081 | } 3082 | 3083 | .tlui-edit-link-dialog__input { 3084 | background-color: var(--color-muted-2); 3085 | flex-grow: 2; 3086 | border-radius: var(--radius-2); 3087 | padding: 0px var(--space-4); 3088 | } 3089 | 3090 | /* Embed Dialog */ 3091 | 3092 | .tlui-embed__spacer { 3093 | flex-grow: 2; 3094 | min-height: 0px; 3095 | margin-left: calc(-1 * var(--space-4)); 3096 | margin-top: calc(-1 * var(--space-4)); 3097 | pointer-events: none; 3098 | } 3099 | 3100 | .tlui-embed-dialog__list { 3101 | display: flex; 3102 | flex-direction: column; 3103 | padding-bottom: var(--space-5); 3104 | } 3105 | 3106 | .tlui-embed-dialog__item { 3107 | position: relative; 3108 | border: none; 3109 | background: none; 3110 | font-family: inherit; 3111 | display: flex; 3112 | text-align: left; 3113 | gap: var(--space-3); 3114 | margin: 0px -8px; 3115 | cursor: pointer; 3116 | padding: 0px 4px; 3117 | align-items: center; 3118 | color: var(--color-text); 3119 | font-size: var(--font-size-1); 3120 | height: 44px; 3121 | } 3122 | 3123 | @media (hover: hover) { 3124 | .tlui-embed-dialog__item:not(:disabled, :focus-visible):hover::after { 3125 | display: block; 3126 | content: ''; 3127 | position: absolute; 3128 | inset: 4px; 3129 | background-color: var(--color-muted-2); 3130 | border-radius: var(--radius-1); 3131 | } 3132 | } 3133 | 3134 | .tlui-embed-dialog__item__image { 3135 | width: 24px; 3136 | height: 24px; 3137 | display: flex; 3138 | align-items: center; 3139 | justify-content: center; 3140 | background-size: contain; 3141 | background-repeat: no-repeat; 3142 | background-position: center center; 3143 | } 3144 | 3145 | .tlui-embed-dialog__enter { 3146 | display: flex; 3147 | flex-direction: column; 3148 | gap: var(--space-4); 3149 | color: var(--color-text-1); 3150 | } 3151 | 3152 | .tlui-embed-dialog__input { 3153 | background-color: var(--color-muted-2); 3154 | flex-grow: 2; 3155 | border-radius: var(--radius-2); 3156 | padding: 0px var(--space-4); 3157 | } 3158 | 3159 | .tlui-embed-dialog__warning { 3160 | color: var(--color-warn); 3161 | text-shadow: none; 3162 | } 3163 | 3164 | .tlui-embed-dialog__instruction__link { 3165 | display: flex; 3166 | gap: var(--space-1); 3167 | margin-top: var(--space-4); 3168 | } 3169 | 3170 | .tlui-embed-dialog__enter a { 3171 | color: var(--color-text-1); 3172 | } 3173 | 3174 | .tlui-following-indicator { 3175 | display: block; 3176 | position: absolute; 3177 | inset: 0px; 3178 | border-width: 2px; 3179 | border-style: solid; 3180 | z-index: var(--layer-following-indicator); 3181 | pointer-events: none; 3182 | } 3183 | 3184 | /* ---------------- Offline Indicator --------------- */ 3185 | 3186 | .tlui-offline-indicator { 3187 | display: flex; 3188 | flex-direction: row; 3189 | gap: var(--space-3); 3190 | color: var(--color-text); 3191 | background-color: var(--color-low); 3192 | border: 3px solid var(--color-background); 3193 | padding: 0px var(--space-5); 3194 | height: 42px; 3195 | align-items: center; 3196 | justify-content: center; 3197 | border-radius: 99px; 3198 | opacity: 0; 3199 | animation: fade-in; 3200 | animation-duration: 0.12s; 3201 | animation-delay: 2s; 3202 | animation-fill-mode: forwards; 3203 | } 3204 | 3205 | /* --------------- Keyboard shortcuts --------------- */ 3206 | 3207 | .tlui-shortcuts-dialog__header { 3208 | border-bottom: 1px solid var(--color-divider); 3209 | } 3210 | 3211 | .tlui-shortcuts-dialog__body { 3212 | position: relative; 3213 | columns: 1; 3214 | column-gap: var(--space-9); 3215 | pointer-events: all; 3216 | touch-action: auto; 3217 | } 3218 | 3219 | @media (min-width: 475px) { 3220 | .tlui-shortcuts-dialog__body { 3221 | columns: 2; 3222 | column-gap: var(--space-9); 3223 | } 3224 | } 3225 | 3226 | @media (min-width: 960px) { 3227 | .tlui-shortcuts-dialog__body { 3228 | columns: 3; 3229 | column-gap: var(--space-9); 3230 | } 3231 | } 3232 | 3233 | .tlui-shortcuts-dialog__group { 3234 | break-inside: avoid-column; 3235 | padding-bottom: var(--space-6); 3236 | } 3237 | 3238 | .tlui-shortcuts-dialog__group__title { 3239 | font-size: inherit; 3240 | font-weight: inherit; 3241 | margin: 0px; 3242 | color: var(--color-text-3); 3243 | height: 32px; 3244 | display: flex; 3245 | align-items: center; 3246 | } 3247 | 3248 | .tlui-shortcuts-dialog__group__content { 3249 | display: flex; 3250 | flex-direction: column; 3251 | color: var(--color-text-1); 3252 | } 3253 | 3254 | .tlui-shortcuts-dialog__key-pair { 3255 | display: flex; 3256 | gap: var(--space-4); 3257 | align-items: center; 3258 | justify-content: space-between; 3259 | height: 32px; 3260 | } 3261 | 3262 | .tlui-shortcuts-dialog__key-pair__key { 3263 | flex: 1; 3264 | font-size: 12px; 3265 | } 3266 | 3267 | -------------------------------------------------------------------------------- /tldraw_whiteboard/public/js/whiteboard/App.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tldraw } from "@tldraw/tldraw"; 3 | import DBStateManager from "./DBStateManager"; 4 | 5 | export function App() { 6 | return ( 7 |