├── LICENSE ├── MANIFEST.in ├── README.md ├── dynamic_tabs ├── __init__.py ├── frontend │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── bootstrap.min.css │ │ └── index.html │ ├── src │ │ ├── DynamicTabs.tsx │ │ ├── icon.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── style.css │ └── tsconfig.json └── iFrame.css ├── setup.py └── videos ├── Already-have-tabs-saved.gif ├── General-demonstration-pt1.gif ├── General-demonstration-pt2.gif ├── Mobile-support.gif └── empty /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2021 Streamlit Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include dynamic_tabs/frontend/build * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # streamlit-dynamic-tabs 2 | Create and close tabs 3 | 4 | ![General-demonstration-pt1.gif](./videos/General-demonstration-pt1.gif) 5 | ![General-demonstration-pt2.gif](./videos/General-demonstration-pt2.gif) 6 | 7 | If your users already have tabs, load them up when the app loads: 8 | 9 | ![Already-have-tabs-saved.gif](./videos/Already-have-tabs-saved.gif) 10 | 11 | Mobile support - more compact and bouncy add tab button interaction 12 | 13 | ![Mobile-support.gif](./videos/Mobile-support.gif) 14 | 15 | 16 | This is a component that helps users make and close tabs dynamically. The tab titles and subsequent content can be collated into a list of dictionaries, saved and recalled for users to view as when they need it (of course you would have to create this part yourself but the component allows for this). 17 | 18 | Possibilities: 19 | - Can add saved tab titles to be loaded up for users to view their previously saved tabs 20 | - Change the add button via material ui icons (google) 21 | - Can limit the number of tabs a user can create 22 | - Can change the design of the tabs 23 | - When there is only one tab, the close button does not appear. This is because upon creating a new tab, the tab is active but value is not passed onto streamlit. So create some code to direct users to click on the tab to view content. Wanted to limited the use of Streamlit's api to send values to the python as it creates buggy behvaiour in the component. 24 | 25 | Its built on the streamlit custom components typescript template 26 | 27 | To install it: 28 | 29 | `pip install streamlit-dynamic-tabs` 30 | 31 | **Variables** 32 | 33 | - tabTitle: list of dictionaries [{'title':''}] 34 | - addIcon: The icon used to represent the add new tab button [material icons from google](https://fonts.google.com/icons) 35 | - limitTabs: Whether or not you want to limit the number of tabs that can be created (Boolean) 36 | - numOfTabs: The number of tabs that can be made if 'limitTabs' variable is True 37 | - styles: CSS designs you wish to apply to tabs (follow the style.css file here as a guide for what can be changed) 38 | 39 | I am yet to use the react funcitonality to adjust the iframe as per the streamlit documentation - lazy on my part. For now please download and place the [iFrame.css](https://github.com/Socvest/streamlit-dynamic-tabs/tree/main/dynamic_tabs) file and import it in your app file. 40 | 41 | Examples: 42 | 43 | ``` 44 | import streamlit as st 45 | import time 46 | st.set_page_config(layout="wide") 47 | 48 | st.subheader("Dynamic Tabs") 49 | st.markdown('', unsafe_allow_html=True) 50 | 51 | # If you wish to load up already existing tabs, load them from a database. Below is just a mock up. 52 | existing_tabs = [{'title':'Tab 1'}, {'title':'Tab 2'}] 53 | 54 | # if you wisht to styyle it according to your own specs: 55 | styles = {'dynamic-tabs':{'':''}} 56 | 'all-tabs':{'':''}} 57 | 'individual-tab-container':{'':''}} 58 | 'tab-selected':{'':''}} 59 | 'title-close-save-button-container':{'':''}} 60 | 'title-of-tab':{'':''}} 61 | 'save-button-container':{'':''}} 62 | 'close-btn-container':{'':''}} 63 | 'new-tab-btn-container':{'':''}} 64 | 65 | d_tabs = dynamic_tabs(tabTitle=existing_tabs, limitTabs=False, numOfTabs=0, styles=None, key="foo") 66 | 67 | A main issue with the streamlit components is the component keeps remounting if its clicked rapidly. Its not that bad though and can be avoided generally. Just input some features into your app that prevents this behvaviour from your users. I used the time.sleep which is demonstrated below. 68 | 69 | if d_tabs == 0: 70 | time.sleep(1) 71 | st.info("""Click on a tab to view contents \n - Name tab by clicking in the input area \n - After renaming, click save to save the tab's title \n - To close the tab, hover over the tab click the close button that slides out""") 72 | st.stop() 73 | 74 | elif d_tabs['currentTab']['title'] == "": 75 | time.sleep(1) 76 | st.title("New Tab") 77 | 78 | else: 79 | time.sleep(1) 80 | st.title(st.session_state['foo']['currentTab']['title']) 81 | 82 | if d_tabs['title'] == {insert tab title here} : 83 | st.write(f"Inside tab {st.session_state['foo']['currentTab']['title']") 84 | 85 | ``` 86 | 87 | 88 | -------------------------------------------------------------------------------- /dynamic_tabs/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import streamlit.components.v1 as components 3 | 4 | _RELEASE = True 5 | 6 | if not _RELEASE: 7 | _dynamic_tabs = components.declare_component( 8 | 9 | "dynamic_tabs", 10 | 11 | url="http://localhost:3001", 12 | ) 13 | else: 14 | parent_dir = os.path.dirname(os.path.abspath(__file__)) 15 | build_dir = os.path.join(parent_dir, "frontend/build") 16 | _dynamic_tabs = components.declare_component("dynamic_tabs", path=build_dir) 17 | 18 | def dynamic_tabs(tabTitle=[{'title':''}], addIcon='add', limitTabs=False, numOfTabs=None,styles=None, key=None): 19 | 20 | component_value = _dynamic_tabs(tabTitle=tabTitle, addIcon=addIcon, limitTabs=limitTabs, numOfTabs=numOfTabs, styles=styles, key=key, default=0) 21 | 22 | 23 | return component_value 24 | 25 | 26 | if not _RELEASE: 27 | import streamlit as st 28 | import time 29 | st.set_page_config(layout="wide") 30 | 31 | st.subheader("Dynamic Tabs") 32 | st.markdown('', unsafe_allow_html=True) 33 | 34 | styles = {'title-of-tab':{'border': 'solid'}} 35 | 36 | if "tabs" not in st.session_state: 37 | st.session_state['tabs'] = [{'title':''}] 38 | 39 | existing_tabs = [{'title':''}] #[{'title':'Tab 1'}, {'title':'Tab 2'}] 40 | 41 | d_tabs = dynamic_tabs(tabTitle=existing_tabs, limitTabs=False, numOfTabs=0, styles=None, key="foo") 42 | 43 | if d_tabs == 0: 44 | time.sleep(1) 45 | st.info("""Click on a tab to view contents \n - Name tab by clicking in the input area \n - After renaming, click save to save the tab's title \n - To close the tab, hover over the tab click the close button that slides out""") 46 | st.stop() 47 | 48 | elif d_tabs['title'] == "": 49 | time.sleep(1) 50 | st.title("New Tab") 51 | 52 | else: 53 | time.sleep(1) 54 | title_placeholder = st.empty() 55 | title_placeholder.title(d_tabs['title']) 56 | if d_tabs['title'] == "": 57 | title_placeholder.title('New Tab') 58 | st.info("Create new tabs") 59 | if d_tabs != "": 60 | st.write("Inside tab") 61 | # what you want to show 62 | #st.stop() 63 | -------------------------------------------------------------------------------- /dynamic_tabs/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamlit_component_template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/jest": "^24.0.0", 7 | "@types/node": "^12.0.0", 8 | "@types/react": "^16.9.0", 9 | "@types/react-dom": "^16.9.0", 10 | "glamor": "^2.20.40", 11 | "jquery": "^3.6.0", 12 | "react": "^16.13.1", 13 | "react-dom": "^16.13.1", 14 | "react-native-uuid": "^2.0.1", 15 | "react-scripts": "3.4.1", 16 | "react-spring": "^9.4.5", 17 | "streamlit-component-lib": "^1.3.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "homepage": "." 41 | } 42 | -------------------------------------------------------------------------------- /dynamic_tabs/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Streamlit Component 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /dynamic_tabs/frontend/src/DynamicTabs.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Streamlit, 3 | ComponentProps, 4 | StreamlitComponentBase, 5 | withStreamlitConnection, 6 | } from "streamlit-component-lib" 7 | import React, { ReactNode, useState } from "react" 8 | import "./style.css" 9 | import "./icon.css" 10 | import { style } from 'glamor'; 11 | 12 | function DynamicTabs (props: ComponentProps) { 13 | 14 | const { args } = props 15 | const tabTitle:any[] = args["tabTitle"] 16 | const limitTabs:boolean = args['limitTabs'] 17 | const numOfTabs:number = args['numOfTabs'] 18 | const addIcon:string = args['addIcon'] 19 | const styles:any = args['styles'] || {} 20 | 21 | const [tab, setTab] = useState(tabTitle) 22 | const [btnState, setBtnState] = useState({activeTabId: null}) 23 | const [inputClicked, setInputClicked] = useState({index:0, clicked:false}) 24 | const [limitNumberOfTabs, setLimitNumberOfTabs] = useState({status: limitTabs, numOfTabs:numOfTabs}) 25 | const [isHovering, setIsHovering] = useState({index:0, status:false}); 26 | 27 | const newTab = () => { 28 | setTab([...tab, {title: ""}]) 29 | 30 | const output = tab.length === 0 ? null : btnState.activeTabId 31 | setBtnState({activeTabId:output}) 32 | 33 | 34 | } 35 | 36 | const deleteTab = (index:number) => { 37 | const tabList = [...tab] 38 | tabList.splice(index, 1) 39 | setTab(tabList) 40 | 41 | let bState = null 42 | if (index < btnState.activeTabId) { 43 | bState = btnState.activeTabId - 1; 44 | } else if (index > btnState.activeTabId) { 45 | bState = btnState.activeTabId; 46 | } else if (index !== 0) { 47 | bState = btnState.activeTabId-1; 48 | } else if (index === 0) { 49 | bState = index; 50 | } 51 | 52 | setBtnState({activeTabId: bState}) 53 | 54 | const output = tabList[bState] 55 | Streamlit.setComponentValue({currentTab:output, deletedTab:{title:'None'}}) 56 | Streamlit.setComponentReady() 57 | } 58 | 59 | const handleTabChange = (e:React.ChangeEvent, index:number) => { 60 | const {name, value} = e.target 61 | const list = [...tab] 62 | list[index][name] = value 63 | setTab(list) 64 | } 65 | 66 | function saveTitle(index:number){ 67 | 68 | let output = tab[index] 69 | Streamlit.setComponentValue({currentTab:output, deletedTab:{title:'None'}}) 70 | Streamlit.setComponentReady() 71 | setInputClicked({index:index, clicked:false}) 72 | } 73 | 74 | const updateActiveId = (id:number): void => { 75 | 76 | setBtnState({activeTabId:id}) 77 | const output = tab[id] 78 | Streamlit.setComponentValue({currentTab:output, deletedTab:{title:'None'}}) 79 | Streamlit.setComponentReady() 80 | 81 | }; 82 | 83 | const activeTabName = (id:number) => { 84 | if (id === btnState.activeTabId) { 85 | return "individual-tab-container tab-selected"; 86 | } else { 87 | return "individual-tab-container"; 88 | } 89 | }; 90 | 91 | const hideClose = (index:number) => { 92 | if (isHovering.index === index && isHovering.status === true) { 93 | return "fade-in 0.3s ease" 94 | } else { 95 | return "fade-out 0.3s ease" 96 | } 97 | 98 | } 99 | 100 | const handleMouseOver = (index:number) => { 101 | setIsHovering({index:index, status:true}); 102 | }; 103 | 104 | const handleMouseOut = (index:number) => { 105 | setIsHovering({index:index, status:false}); 106 | }; 107 | 108 | function inputClickHandler(index:number){ 109 | setInputClicked({index:index, clicked:true}) 110 | 111 | } 112 | 113 | return ( 114 |
115 |
    116 | {tab.map((tabTitle, index) => ( 117 | handleMouseOver(index)} onMouseLeave={() => handleMouseOut(index)}> 118 | updateActiveId(index)}> 119 |
  • 120 | 121 | inputClickHandler(index)} 132 | onChange={(e) => handleTabChange(e, index)}/> 133 | 134 |
  • 135 |
    136 | { inputClicked.index === index && inputClicked.clicked === true && 137 | 138 |
    139 |
    } 140 | {isHovering.index === index && isHovering.status === true && tab.length > 1 && 141 | 142 |
    143 |
    } 146 | 147 |
    148 | ))} 149 |
150 | { (limitNumberOfTabs.status === false || tab.length !== limitNumberOfTabs.numOfTabs) && 151 | 152 | 154 | } 155 |
156 | ) 157 | } 158 | 159 | export default withStreamlitConnection(DynamicTabs); 160 | -------------------------------------------------------------------------------- /dynamic_tabs/frontend/src/icon.css: -------------------------------------------------------------------------------- 1 | /* fallback */ 2 | @font-face { 3 | font-family: 'Material Icons'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: url(https://fonts.gstatic.com/s/materialicons/v121/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2) format('woff2'); 7 | } 8 | 9 | .material-icons { 10 | font-family: 'Material Icons'; 11 | font-weight: normal; 12 | font-style: normal; 13 | font-size: 24px; 14 | line-height: 1; 15 | letter-spacing: normal; 16 | text-transform: none; 17 | display: inline-block; 18 | white-space: nowrap; 19 | word-wrap: normal; 20 | direction: ltr; 21 | -webkit-font-feature-settings: 'liga'; 22 | -webkit-font-smoothing: antialiased; 23 | } -------------------------------------------------------------------------------- /dynamic_tabs/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import DynamicTabs from "./DynamicTabs" 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById("root") 10 | ) 11 | -------------------------------------------------------------------------------- /dynamic_tabs/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /dynamic_tabs/frontend/src/style.css: -------------------------------------------------------------------------------- 1 | .dynamic-tabs{ 2 | position: relative; 3 | display: flex; 4 | flex-wrap: nowrap; 5 | overflow-x: auto; 6 | height: 120px; 7 | width:94%; 8 | 9 | } 10 | .all-tabs{ 11 | display: flex; 12 | height: 80px; 13 | margin-left: -36px; 14 | } 15 | 16 | 17 | .tab-container{ 18 | display: flex; 19 | height: 110%; 20 | cursor: pointer; 21 | } 22 | 23 | @keyframes slide-in { 24 | 0% { 25 | opacity: 0; 26 | transform: translateX(20px); 27 | } 28 | 100% { 29 | opacity: 1; 30 | transform: translateX(0); 31 | } 32 | } 33 | 34 | @keyframes creep-in { 35 | 0% { 36 | opacity: 0; 37 | } 38 | 50%{ 39 | opacity: 0.5; 40 | } 41 | 100% { 42 | opacity: 1; 43 | } 44 | } 45 | 46 | .individual-tab-container{ 47 | display: flex; 48 | border-bottom: 2px solid #d3d2d2; 49 | margin-top: 7px; 50 | width:360px; 51 | animation:slide-in 0.5s ease, creep-in 0.4s ease; 52 | 53 | } 54 | 55 | li { 56 | list-style-type: none; 57 | } 58 | 59 | .title-close-save-button-container{ 60 | display: flex; 61 | margin-top:6.5%; 62 | } 63 | 64 | input{ 65 | margin-left:2%; 66 | margin-top:4%; 67 | border:none; 68 | font-family: 'Hi Melody', cursive; 69 | font-size: 14px; 70 | font-weight:500; 71 | } 72 | 73 | 74 | input:focus{ 75 | outline:none; 76 | } 77 | 78 | button{ 79 | background: none; 80 | border: none; 81 | } 82 | 83 | button:active{ 84 | outline: none; 85 | } 86 | 87 | button:focus{ 88 | outline: none; 89 | } 90 | 91 | @keyframes save-in { 92 | 0% { 93 | opacity: 0; 94 | transform: translateY(-100%); 95 | } 96 | 100% { 97 | opacity: 1; 98 | transform: translateY(5px); 99 | } 100 | } 101 | 102 | .save-button-container{ 103 | margin-top:6%; 104 | font-family: 'Hi Melody', cursive; 105 | font-weight:300; 106 | 107 | } 108 | 109 | @keyframes fade-in { 110 | 0% { 111 | opacity: 0; 112 | transform: translateX(-20px); 113 | } 114 | 100% { 115 | opacity: 1; 116 | transform: translateX(0px); 117 | 118 | } 119 | } 120 | 121 | @keyframes fade-out { 122 | 0% { 123 | transform: translateX(0px); 124 | opacity: 1; 125 | } 126 | 100% { 127 | transform: translateX(-20px); 128 | opacity: 0; 129 | } 130 | } 131 | 132 | 133 | .close-btn-container{ 134 | margin-top:6%; 135 | } 136 | 137 | 138 | .new-tab-btn-container{ 139 | display: flex; 140 | align-items: flex-end; 141 | position: fixed; 142 | margin-left: 95%; 143 | top:7%; 144 | height:60px; 145 | } 146 | 147 | .new-tab-btn{ 148 | background: none; 149 | border: none; 150 | border-radius: 50%; 151 | text-align: center; 152 | height: 40px; 153 | width: 41px; 154 | } 155 | 156 | .new-tab-btn:hover{ 157 | background-color: #30575F; 158 | 159 | } 160 | 161 | .new-tab-btn:hover #add-btn{ 162 | color:white; 163 | margin-top: 5px; 164 | } 165 | 166 | .tab-selected{ 167 | border-bottom: 8px solid #30575F; 168 | transition: 0.3s ease; 169 | 170 | } 171 | 172 | .tab-selected input{ 173 | font-weight:bold; 174 | margin-top:2%; 175 | transition: 0.4s ease; 176 | } 177 | 178 | .tab-selected .save-button-container{ 179 | margin-top:4%; 180 | transition: 0.4s ease; 181 | } 182 | 183 | ::-webkit-scrollbar{ 184 | height: 5px; 185 | background-color: transparent; 186 | } 187 | 188 | ::-webkit-scrollbar-track{ 189 | margin-left: .3rem; 190 | } 191 | 192 | ::-webkit-scrollbar-thumb{ 193 | background-color: #467d88; 194 | border-radius: 100vw; 195 | 196 | } 197 | 198 | ::-webkit-scrollbar-thumb:hover{ 199 | background-color: #30575F; 200 | } 201 | 202 | ::-webkit-scrollbar-corner{ 203 | margin-left: 5px; 204 | } 205 | 206 | /* Smartphones (portrait and landscape) ----------- */ 207 | @media only screen and (min-device-width : 320px) and (max-device-width : 480px) { 208 | 209 | .dynamic-tabs{ 210 | position: relative; 211 | display: flex; 212 | flex-wrap: nowrap; 213 | overflow-x: auto; 214 | height: 78px; 215 | width:95%; 216 | 217 | } 218 | 219 | .all-tabs{ 220 | height:50px; 221 | } 222 | 223 | .tab-container { 224 | margin-top: 21px; 225 | height:75%; 226 | } 227 | 228 | .individual-tab-container{ 229 | display: flex; 230 | border-bottom: 2px solid #d3d2d2; 231 | margin-top: 6px; 232 | height: 29px; 233 | width: 145px; 234 | animation: slide-in 0.5s ease, creep-in 0.4s ease; 235 | 236 | } 237 | 238 | li{ 239 | margin-top: 5px; 240 | height: 17px; 241 | } 242 | 243 | input{ 244 | margin-top: -4px; 245 | width: 95px; 246 | font-family: 'Hi Melody', cursive; 247 | font-size: 8px; 248 | font-weight:500; 249 | } 250 | 251 | input::placeholder{ 252 | content: "Hiya"; 253 | } 254 | 255 | .tab-selected input{ 256 | margin-top:-4px; 257 | 258 | } 259 | 260 | .tab-selected{ 261 | border-bottom:3.5px solid #30575f; 262 | transition: 0.3s ease; 263 | } 264 | 265 | .save-button-container{ 266 | margin-top:6.3%; 267 | font-size: 7.5px; 268 | } 269 | 270 | .close-btn-container{ 271 | margin-top:6%; 272 | font-size: 8.5px; 273 | } 274 | 275 | .new-tab-btn-container{ 276 | display: flex; 277 | align-items:flex-end; 278 | top: 2px; 279 | /* margin-left: 5px; */ 280 | position: fixed; 281 | height:60px; 282 | } 283 | 284 | .new-tab-btn:hover { 285 | background-color: transparent; 286 | 287 | } 288 | #add-btn{ 289 | top:10px; 290 | margin-left: -2px; 291 | display:flex; 292 | font-size: 13px; 293 | margin-top: -1px; 294 | transition: 0.2s ease-in-out; 295 | } 296 | 297 | #add-btn:active { 298 | transform: scale(1.3); 299 | transition: .1s; 300 | } 301 | 302 | .new-tab-btn:hover{ 303 | background-color: transparent; 304 | } 305 | 306 | .new-tab-btn:hover #add-btn{ 307 | 308 | color:#30575F; 309 | margin-top: 1px; 310 | 311 | } 312 | } 313 | 314 | /* Samsung (landscape) ----------- */ 315 | @media only screen and (min-device-width : 570px) and (max-device-width : 915px) { 316 | 317 | 318 | .dynamic-tabs{ 319 | position: relative; 320 | display: flex; 321 | flex-wrap: nowrap; 322 | overflow-x: auto; 323 | height: 78px; 324 | width:95%; 325 | 326 | } 327 | 328 | .all-tabs{ 329 | height:50px; 330 | } 331 | 332 | .tab-container { 333 | margin-top: 21px; 334 | height:75%; 335 | } 336 | 337 | .individual-tab-container{ 338 | display: flex; 339 | border-bottom: 2px solid #d3d2d2; 340 | margin-top: 6px; 341 | height: 29px; 342 | width: 141px; 343 | animation: slide-in 0.5s ease, creep-in 0.4s ease; 344 | 345 | } 346 | 347 | li{ 348 | margin-top: 5px; 349 | height: 17px; 350 | } 351 | 352 | input{ 353 | margin-top: -4px; 354 | width: 91px; 355 | font-family: 'Hi Melody', cursive; 356 | font-size: 8px; 357 | font-weight:500; 358 | } 359 | 360 | input::placeholder{ 361 | content: "Hiya"; 362 | } 363 | 364 | .tab-selected input{ 365 | margin-top:-4px; 366 | 367 | } 368 | 369 | .tab-selected{ 370 | border-bottom:3.5px solid #30575f; 371 | transition: 0.3s ease; 372 | } 373 | 374 | .save-button-container{ 375 | margin-top:6.3%; 376 | font-size: 7.5px; 377 | } 378 | 379 | .close-btn-container{ 380 | margin-top:6%; 381 | font-size: 8.5px; 382 | } 383 | 384 | .new-tab-btn-container{ 385 | display: flex; 386 | align-items: flex-end; 387 | top: 2px; 388 | /* margin-left: 511px; */ 389 | position: fixed; 390 | height:60px; 391 | } 392 | 393 | .new-tab-btn:hover { 394 | background-color: transparent; 395 | 396 | } 397 | #add-btn{ 398 | top:10px; 399 | margin-left: -2px; 400 | display:flex; 401 | font-size: 13px; 402 | margin-top: -1px; 403 | transition: 0.2s ease-in-out; 404 | } 405 | 406 | #add-btn:active { 407 | transform: scale(1.3); 408 | transition: .1s; 409 | } 410 | 411 | .new-tab-btn:hover{ 412 | background-color: transparent; 413 | } 414 | 415 | .new-tab-btn:hover #add-btn{ 416 | 417 | color:#30575F; 418 | margin-top: 1px; 419 | 420 | } 421 | 422 | } 423 | 424 | -------------------------------------------------------------------------------- /dynamic_tabs/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /dynamic_tabs/iFrame.css: -------------------------------------------------------------------------------- 1 | iframe { 2 | width: 100%; 3 | height: 122px; 4 | 5 | } 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name="streamlit-dynamic-tabs", 5 | version="0.0.2", 6 | author="", 7 | author_email="", 8 | description="", 9 | long_description="", 10 | long_description_content_type="text/plain", 11 | url="", 12 | packages=setuptools.find_packages(), 13 | include_package_data=True, 14 | classifiers=[], 15 | python_requires=">=3.6", 16 | install_requires=[ 17 | # By definition, a Custom Component depends on Streamlit. 18 | # If your component has other Python dependencies, list 19 | # them here. 20 | "streamlit >= 0.63", 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /videos/Already-have-tabs-saved.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Socvest/streamlit-dynamic-tabs/2c8d7bb6e441a194fc72feb396516278d37a3a9f/videos/Already-have-tabs-saved.gif -------------------------------------------------------------------------------- /videos/General-demonstration-pt1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Socvest/streamlit-dynamic-tabs/2c8d7bb6e441a194fc72feb396516278d37a3a9f/videos/General-demonstration-pt1.gif -------------------------------------------------------------------------------- /videos/General-demonstration-pt2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Socvest/streamlit-dynamic-tabs/2c8d7bb6e441a194fc72feb396516278d37a3a9f/videos/General-demonstration-pt2.gif -------------------------------------------------------------------------------- /videos/Mobile-support.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Socvest/streamlit-dynamic-tabs/2c8d7bb6e441a194fc72feb396516278d37a3a9f/videos/Mobile-support.gif -------------------------------------------------------------------------------- /videos/empty: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------