Interestingly, Home Assistant 0.106.3 has a Coronavirus integration. Although perhaps a little overboard, it might be interesting to incorporate this into a Tileboard page. Any advice on how to do that?
I just updated to see what it gives you. It’s just 4 sensors. Confirmed, current, recovered and deaths. A TYPES.TEXT_LIST tile would probably be easiest. You could do 4 sensor tiles for each. You could dump the data into a grafana graph and use the images as a background. If you want images or graphs, it might just be easier to use images from an internet site that’s actively updating stats then use those with camera tiles.
I just added this to my Tileboard. I added a value function to add the thousands separator…
{
position: [0, 3],
width: 2,
height: .75,
title: 'US SARS-Cov-2 Information',
id: {},
type: TYPES.TEXT_LIST,
state: false,
list: [
{
title: 'US Confirmed Cases',
//value: 'sensor.us_coronavirus_confirmed.state'
value: function () {
return Number(this.states['sensor.us_coronavirus_confirmed'].state).toLocaleString();
}
},
{
title: 'US Current Cases',
//value: '&sensor.us_coronavirus_current.state'
value: function () {
return Number(this.states['sensor.us_coronavirus_current'].state).toLocaleString();
}
},
{
title: 'US Current Deaths',
//value: '&sensor.us_coronavirus_deaths.state'
value: function () {
return Number(this.states['sensor.us_coronavirus_deaths'].state).toLocaleString();
}
},
{
title: 'US Current Recovered',
//value: '&sensor.us_coronavirus_recovered.state'
value: function () {
return Number(this.states['sensor.us_coronavirus_recovered'].state).toLocaleString();
}
}, ]
},
{
position: [0, 3],
width: 2,
height: .75,
title: 'Worldwide SARS-Cov-2 Information',
id: {},
type: TYPES.TEXT_LIST,
state: false,
list: [
{
title: 'Worldwide Confirmed Cases',
//value: 'sensor.worldwide_coronavirus_confirmed.state'
value: function () {
return Number(this.states['sensor.worldwide_coronavirus_confirmed'].state).toLocaleString();
}
},
{
title: 'Worldwide Current Cases',
//value: '&sensor.worldwide_coronavirus_current.state'
value: function () {
return Number(this.states['sensor.worldwide_coronavirus_current'].state).toLocaleString();
}
},
{
title: 'Worldwide Current Deaths',
//value: '&sensor.worldwide_coronavirus_deaths.state'
value: function () {
return Number(this.states['sensor.worldwide_coronavirus_deaths'].state).toLocaleString();
}
},
{
title: 'Worldwide Current Recovered',
//value: '&sensor.worldwide_coronavirus_recovered.state'
value: function () {
return Number(this.states['sensor.worldwide_coronavirus_recovered'].state).toLocaleString();
}
}, ]
},
Thanks, I implemented it as you provided, although you have a minor typo. You have both the US and Worldwide statistics going to the same tile ( position: [0, 3]
) so they overwrite each other. I fixed that and it works great.
I’ve been trying to add some sort of light brightness popup which is similar to what you are trying to do.
I try try and code something myself but didn’t get very far, but if you to manage to do better than me then I’d be interested to see how it’s done.
Excellent. I have them in two separate groups on the same page which is why the position is the same. I also have a custom css that I didn’t include that makes the text size smaller to fit on the .75 height tiles. You probably had to increase the height a bit for standard size text to fit.
Can someone please assist with how to script a function to detect which page is currently open?
I have searched the github page and through this thread but couldn’t find anything.
I’m on it. This is what I have so far. I added a generic fully customizable popup that can be added to any tile (the tile layout and contents can change from tile to tile, to display context sensitive information). I still have some positioning problems, so I will have to dive into the CSS/less files to fix this. I’ll try to find some time this upcoming weekend to finish this.
Here’s a very early example for a popup that appears when tapping a weather tile and shows more detailed weather data and forecast. The margins and tile positioning is still wrong, but it works.
I don’t see a way to get it directly. There’s an activePage variable in main.js that keeps track of it. You could create a global variable $scope.activePage in main.js and set it to page in in the $scope.openPage function.
isn’t it simple to use cover_toggle type and have icon change based on garage door statue closed vs open?
this is my code for garage doors in Tileboard.
{
position: [0, 0], // [x, y]
width: 1,
type: TYPES.COVER_TOGGLE,
title: 'Garage Door 1',
id: 'cover.double',
states: {open: 'Opened', closed: 'Closed', unknown: 'Unknown'},
icons: {open: "mdi-garage-open", closed: "mdi-garage", unknown: "mdi-garage-alert"},
state: false, // hidding state
customStyles: function(item, entity){
if (entity.state === 'closed') {return {'backgroundColor': '#27B80D',};}
else if (entity.state === 'open') {return {'backgroundColor': '#B80D0D',};}
else {return { 'backgroundColor': '#FFA100',};}
}
},
Can someone help me here on what i am doing wrong?
I have added another selection box for the fan speed on the A/C,
Everything looked ok, Works fine.
But when i added another Climate Tile I noticed, that if clicking on the fan mode,
will open the Selection box on all 3 Fans (rather only on the selected active one).
This is not the case with the A/C Mode, It works perfect).
Fan Mode:
AC Mode:
Code:
<div ng-if="item.type === TYPES.CLIMATE"
class="item-entity-container">
<div>
<div class="item-button -bottom-left"
ng-if="entity.attributes.temperature && entity.state !== 'off'"
ng-click="increaseClimateTemp($event, item, entity)">
<i class="mdi mdi-arrow-up-bold"></i>
</div>
<div class="item-button -center-left"
ng-if="entity.attributes.temperature && entity.state !== 'off'"
ng-click="decreaseClimateTemp($event, item, entity)">
<i class="mdi mdi-arrow-down-bold"></i>
</div>
</div>
<div class="item-climate">
<div class="item-climate--target">
<span ng-bind="climateTarget(item, entity)"></span>
<span ng-if="(_unit = entityUnit(item, entity))"
class="item-climate--target--unit" ng-bind="_unit"></span>
</div>
<!-- Climate Mode (Head,Dry, Cool, Fan Only...) -->
<div class="item-climate--mode" ng-if="entity.attributes.temperature"
ng-click="openSelect(item)">
<span ng-bind="entity.state"></span>
</div>
<!-- Climate Fan Mode (Low, Medium High....) -->
<div class="item-climate--mode" ng-if="entity.attributes.temperature"
ng-click="openSelect(seconditem)">
<span ng-bind="entity.attributes.fan_mode"></span>
</div>
</div>
<!-- Climate Mode (Head,Dry, Cool, Fan Only...) -->
<div ng-if="selectOpened(item)" class="item-select"
ng-style="itemSelectStyles(entity, entity.attributes.hvac_modes)">
<div class="item-select--option"
ng-repeat="option in entity.attributes.hvac_modes track by $index"
ng-class="{'-active': option === entity.state}"
ng-click="setClimateMode($event, item, entity, option)">
<span ng-bind="option"></span>
</div>
</div>
<!-- Climate Fan Mode (Low, Medium High....) -->
<div ng-if="selectOpened(seconditem)" class="item-select"
ng-style="itemSelectStyles(entity, entity.attributes.fan_modes)">
<div class="item-select--option"
ng-repeat="option in entity.attributes.fan_modes track by $index"
ng-class="{'-active': option === entity.attributes.fan_mode}"
ng-click="setClimateMode($event, item, entity, option)">
<span ng-bind="option"></span>
</div>
</div>
</div>
how to work with more than one js file? example leaving a page in each file. I followed the link below, but I don’t know how to make the other file, the error.
I’m not sure there’s a easy way to do that because you’d be trying to break a single variable up.
I use what @diggerdanh posted here: https://community.home-assistant.io/t/tileboard-new-dashboard-for-homeassistant/57173/2152
You can then create the tiles using one liners and you can pass multiple elements and the function will parse through them all.
I use angular.merge with mine as I need to set variables in a function to what I pass to the build tile function then merge the objects together. I was able to reduce my code by at least 2000 lines using this method. When I get back, I’ll post sample code.
Hi, how did you blur the background of the popup?
Sample code that I use to display 16 camera tiles.
tiles = {};
tiles.cameras_house = {
height: .75,
width: 1.08,
type: TYPES.CAMERA,
id: {},
bgSize: 'cover',
state: false,
}
function buildCameraTile( thumb, video, tileDefinition, options ){
var z = {
fullscreen: {
type: TYPES.CAMERA,
id: {},
bgSize: 'contain',
filter: function () {
return video;
},
},
filter: function () {
return thumb;
}
};
var tile = angular.copy( tileDefinition );
Object.keys( options ).forEach( k => {
tile[ k ] = options[ k ]
} );
angular.merge( tile , z );
return tile;
};
...
buildCameraTile( 'http://192.168.10.12/TileBoard/CameraThumbs/camera05.jpg', 'http://192.168.6.10:8090/channel05.mjpeg', tiles.cameras_house, { position: [0, 0], title: '05 A/C Units' } ),
buildCameraTile( 'http://192.168.10.12/TileBoard/CameraThumbs/camera14.jpg', 'http://192.168.6.10:8090/channel14.mjpeg', tiles.cameras_house, { position: [0, .75], title: '14 Attached Garage' } ),
buildCameraTile( 'http://192.168.10.12/TileBoard/CameraThumbs/camera15.jpg', 'http://192.168.6.10:8090/channel15.mjpeg', tiles.cameras_house, { position: [0, 1.5], title: '15 Basement' } ),
...
You can also pass additional name/value pairs that your tile supports: { position: [0, 1.5], title: ‘15 Basement’ , refresh: 30000 , name: value, etc: ‘etc’ }
Nice, the CSS margins etc. isn’t really an issue, the fact that it works at all is great. I was only able to make a popup thing but couldn’t get anything to appear in the popup. I was trying to make work in a similar way to the history graph popups.
If you want me to test or fix the CSS I’m happy to take a look if you want.
Thx for the tip … but do you think that it is possible not only include/merge some vars but bigger portion of config e.g. entire tile/button or section ?
I tried to use loadJS function from the link you provided but for some reason it does not work for me
Sorry for maybe trivial question but I’m not a programmer and sometimes it is not that easy for me to understand how to implement/use things
the tiles is a vars itself. Define Tile var and merge it into Group. Groups in turn is a vars too. You can define Tiles, merge them into Group, then merge Groups into Page. and same for Pages merged into CONFIG.
Only issue here is the visiblity of variables, but I can not much help here as I am not very familiar with JS.
I’m having problems with the files to be loaded, in your case, what would the tiles.js file look like?
Can anyone give me a hint about what I am missing here?
When I initially load the gauges, the circle/gauge is aligned with the state value. After some time however, the gauge start to appear totally random, like in the screenshot below. The values that are in the 50s should show a half gauge, but they appear full at 100% except for one.
Here is the code:
{
position: [0, 0],
width: 1,
height: 1,
title: 'Soveværelset',
subtitle: '',
state: null,
type: TYPES.GAUGE,
id: 'sensor.netatmo_bedroom_temperature', // Assign the sensor you want to display on the gauge
value: function(item, entity) {
return entity.state;
},
settings: {
// size: 115, // Defaults to 50% of either height or width, whichever is smaller
type: 'full', // Options are: 'full', 'semi', and 'arch'. Defaults to 'full'
min: -20, // Defaults to 0
max: 40, // Defaults to 100
cap: 'round', // Options are: 'round', 'butt'. Defaults to 'butt'
thick: 8, // Defaults to 6
// label: 'Soveværelset', // Defaults to undefined
append: '@attributes.unit_of_measurement', // Defaults to undefined
// prepend: , // Defaults to undefined
duration: 1500, // Defaults to 1500ms
thresholds: { 0: { color: 'blue'}, 18: { color: 'green'}, 25: { color: 'red' } }, // Defaults to undefined
labelOnly: false, // Defaults to false
foregroundColor: 'rgba(0, 150, 136, 1)', // Defaults to rgba(0, 150, 136, 1)
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Defaults to rgba(0, 0, 0, 0.1)
fractionSize: 0, // Number of decimal places to round the number to. Defaults to current locale formatting
},
},
{
position: [1, 0],
width: 1,
height: 1,
title: 'Soveværelset',
subtitle: '',
state: null,
type: TYPES.GAUGE,
id: 'sensor.netatmo_bedroom_humidity', // Assign the sensor you want to display on the gauge
value: function(item, entity) {
return entity.state;
},
settings: {
// size: 100, // Defaults to 50% of either height or width, whichever is smaller
type: 'full', // Options are: 'full', 'semi', and 'arch'. Defaults to 'full'
min: 30, // Defaults to 0
max: 80, // Defaults to 100
cap: 'round', // Options are: 'round', 'butt'. Defaults to 'butt'
thick: 8, // Defaults to 6
// label: 'Soveværelset', // Defaults to undefined
append: '@attributes.unit_of_measurement', // Defaults to undefined
// prepend: , // Defaults to undefined
duration: 1500, // Defaults to 1500ms
thresholds: { 30: { color: 'green'}, 50: { color: 'yellow' }, 60: { color: 'red' } }, // Defaults to undefined
labelOnly: false, // Defaults to false
foregroundColor: 'rgba(0, 150, 136, 1)', // Defaults to rgba(0, 150, 136, 1)
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Defaults to rgba(0, 0, 0, 0.1)
fractionSize: 0, // Number of decimal places to round the number to. Defaults to current locale formatting
},
},
{
position: [0, 1],
width: 1,
height: 1,
title: 'Karla / Lærke',
subtitle: '',
state: null,
type: TYPES.GAUGE,
id: 'sensor.netatmo_childrens_room_temperature', // Assign the sensor you want to display on the gauge
value: function(item, entity) {
return entity.state;
},
settings: {
// size: 115, // Defaults to 50% of either height or width, whichever is smaller
type: 'full', // Options are: 'full', 'semi', and 'arch'. Defaults to 'full'
min: -20, // Defaults to 0
max: 40, // Defaults to 100
cap: 'round', // Options are: 'round', 'butt'. Defaults to 'butt'
thick: 8, // Defaults to 6
// label: 'Soveværelset', // Defaults to undefined
append: '@attributes.unit_of_measurement', // Defaults to undefined
// prepend: , // Defaults to undefined
duration: 1500, // Defaults to 1500ms
thresholds: { 0: { color: 'blue'}, 18: { color: 'green'}, 25: { color: 'red' } }, // Defaults to undefined
labelOnly: false, // Defaults to false
foregroundColor: 'rgba(0, 150, 136, 1)', // Defaults to rgba(0, 150, 136, 1)
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Defaults to rgba(0, 0, 0, 0.1)
fractionSize: 0, // Number of decimal places to round the number to. Defaults to current locale formatting
},
},
{
position: [1, 1],
width: 1,
height: 1,
title: 'Karla / Lærke',
subtitle: '',
state: null,
type: TYPES.GAUGE,
id: 'sensor.netatmo_childrens_room_humidity', // Assign the sensor you want to display on the gauge
value: function(item, entity) {
return entity.state;
},
settings: {
// size: 100, // Defaults to 50% of either height or width, whichever is smaller
type: 'full', // Options are: 'full', 'semi', and 'arch'. Defaults to 'full'
min: 30, // Defaults to 0
max: 80, // Defaults to 100
cap: 'round', // Options are: 'round', 'butt'. Defaults to 'butt'
thick: 8, // Defaults to 6
// label: 'Soveværelset', // Defaults to undefined
append: '@attributes.unit_of_measurement', // Defaults to undefined
// prepend: , // Defaults to undefined
duration: 1500, // Defaults to 1500ms
thresholds: { 30: { color: 'green'}, 50: { color: 'yellow' }, 60: { color: 'red' } }, // Defaults to undefined
labelOnly: false, // Defaults to false
foregroundColor: 'rgba(0, 150, 136, 1)', // Defaults to rgba(0, 150, 136, 1)
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Defaults to rgba(0, 0, 0, 0.1)
fractionSize: 0, // Number of decimal places to round the number to. Defaults to current locale formatting
},
},
{
position: [0, 2],
width: 1,
height: 1,
title: 'Spisestuen',
subtitle: '',
state: null,
type: TYPES.GAUGE,
id: 'sensor.netatmo_spisestuen_temperature', // Assign the sensor you want to display on the gauge
value: function(item, entity) {
return entity.state;
},
settings: {
// size: 115, // Defaults to 50% of either height or width, whichever is smaller
type: 'full', // Options are: 'full', 'semi', and 'arch'. Defaults to 'full'
min: -20, // Defaults to 0
max: 40, // Defaults to 100
cap: 'round', // Options are: 'round', 'butt'. Defaults to 'butt'
thick: 8, // Defaults to 6
// label: 'Soveværelset', // Defaults to undefined
append: '@attributes.unit_of_measurement', // Defaults to undefined
// prepend: , // Defaults to undefined
duration: 1500, // Defaults to 1500ms
thresholds: { 0: { color: 'blue'}, 18: { color: 'green'}, 25: { color: 'red' } }, // Defaults to undefined
labelOnly: false, // Defaults to false
foregroundColor: 'rgba(0, 150, 136, 1)', // Defaults to rgba(0, 150, 136, 1)
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Defaults to rgba(0, 0, 0, 0.1)
fractionSize: 0, // Number of decimal places to round the number to. Defaults to current locale formatting
},
},
{
position: [1, 2],
width: 1,
height: 1,
title: 'Spisestuen',
subtitle: '',
state: null,
type: TYPES.GAUGE,
id: 'sensor.netatmo_spisestuen_humidity', // Assign the sensor you want to display on the gauge
value: function(item, entity) {
return entity.state;
},
settings: {
// size: 100, // Defaults to 50% of either height or width, whichever is smaller
type: 'full', // Options are: 'full', 'semi', and 'arch'. Defaults to 'full'
min: 30, // Defaults to 0
max: 80, // Defaults to 100
cap: 'round', // Options are: 'round', 'butt'. Defaults to 'butt'
thick: 8, // Defaults to 6
// label: 'Soveværelset', // Defaults to undefined
append: '@attributes.unit_of_measurement', // Defaults to undefined
// prepend: , // Defaults to undefined
duration: 1500, // Defaults to 1500ms
thresholds: { 30: { color: 'green'}, 50: { color: 'yellow' }, 60: { color: 'red' } }, // Defaults to undefined
labelOnly: false, // Defaults to false
foregroundColor: 'rgba(0, 150, 136, 1)', // Defaults to rgba(0, 150, 136, 1)
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Defaults to rgba(0, 0, 0, 0.1)
fractionSize: 0, // Number of decimal places to round the number to. Defaults to current locale formatting
},
},
{
position: [0, 3],
width: 1,
height: 1,
title: 'Stuen',
subtitle: '',
state: null,
type: TYPES.GAUGE,
id: 'sensor.xiaomi_cgg1_temperature', // Assign the sensor you want to display on the gauge
value: function(item, entity) {
return entity.state;
},
settings: {
// size: 115, // Defaults to 50% of either height or width, whichever is smaller
type: 'full', // Options are: 'full', 'semi', and 'arch'. Defaults to 'full'
min: -20, // Defaults to 0
max: 40, // Defaults to 100
cap: 'round', // Options are: 'round', 'butt'. Defaults to 'butt'
thick: 8, // Defaults to 6
// label: 'Soveværelset', // Defaults to undefined
append: '@attributes.unit_of_measurement', // Defaults to undefined
// prepend: , // Defaults to undefined
duration: 1500, // Defaults to 1500ms
thresholds: { 0: { color: 'blue'}, 18: { color: 'green'}, 25: { color: 'red' } }, // Defaults to undefined
labelOnly: false, // Defaults to false
foregroundColor: 'rgba(0, 150, 136, 1)', // Defaults to rgba(0, 150, 136, 1)
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Defaults to rgba(0, 0, 0, 0.1)
fractionSize: 0, // Number of decimal places to round the number to. Defaults to current locale formatting
},
},
{
position: [1, 3],
width: 1,
height: 1,
title: 'Stuen',
subtitle: '',
state: null,
type: TYPES.GAUGE,
id: 'sensor.xiaomi_cgg1_humidity', // Assign the sensor you want to display on the gauge
value: function(item, entity) {
return entity.state;
},
settings: {
// size: 100, // Defaults to 50% of either height or width, whichever is smaller
type: 'full', // Options are: 'full', 'semi', and 'arch'. Defaults to 'full'
min: 30, // Defaults to 0
max: 80, // Defaults to 100
cap: 'round', // Options are: 'round', 'butt'. Defaults to 'butt'
thick: 8, // Defaults to 6
// label: 'Soveværelset', // Defaults to undefined
append: '@attributes.unit_of_measurement', // Defaults to undefined
// prepend: , // Defaults to undefined
duration: 1500, // Defaults to 1500ms
thresholds: { 30: { color: 'green'}, 50: { color: 'yellow' }, 60: { color: 'red' } }, // Defaults to undefined
labelOnly: false, // Defaults to false
foregroundColor: 'rgba(0, 150, 136, 1)', // Defaults to rgba(0, 150, 136, 1)
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Defaults to rgba(0, 0, 0, 0.1)
fractionSize: 0, // Number of decimal places to round the number to. Defaults to current locale formatting
},
},
{
position: [0, 4],
width: 1,
height: 1,
title: 'Gæsterummet',
subtitle: '',
state: null,
type: TYPES.GAUGE,
id: 'sensor.netatmo_guestroom_temperature', // Assign the sensor you want to display on the gauge
value: function(item, entity) {
return entity.state;
},
settings: {
// size: 115, // Defaults to 50% of either height or width, whichever is smaller
type: 'full', // Options are: 'full', 'semi', and 'arch'. Defaults to 'full'
min: -20, // Defaults to 0
max: 40, // Defaults to 100
cap: 'round', // Options are: 'round', 'butt'. Defaults to 'butt'
thick: 8, // Defaults to 6
// label: 'Soveværelset', // Defaults to undefined
append: '@attributes.unit_of_measurement', // Defaults to undefined
// prepend: , // Defaults to undefined
duration: 1500, // Defaults to 1500ms
thresholds: { 0: { color: 'blue'}, 18: { color: 'green'}, 25: { color: 'red' } }, // Defaults to undefined
labelOnly: false, // Defaults to false
foregroundColor: 'rgba(0, 150, 136, 1)', // Defaults to rgba(0, 150, 136, 1)
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Defaults to rgba(0, 0, 0, 0.1)
fractionSize: 0, // Number of decimal places to round the number to. Defaults to current locale formatting
},
},
{
position: [1, 4],
width: 1,
height: 1,
title: 'Gæsterummet',
subtitle: '',
state: null,
type: TYPES.GAUGE,
id: 'sensor.netatmo_guestroom_humidity', // Assign the sensor you want to display on the gauge
value: function(item, entity) {
return entity.state;
},
settings: {
// size: 100, // Defaults to 50% of either height or width, whichever is smaller
type: 'full', // Options are: 'full', 'semi', and 'arch'. Defaults to 'full'
min: 30, // Defaults to 0
max: 80, // Defaults to 100
cap: 'round', // Options are: 'round', 'butt'. Defaults to 'butt'
thick: 8, // Defaults to 6
// label: 'Soveværelset', // Defaults to undefined
append: '@attributes.unit_of_measurement', // Defaults to undefined
// prepend: , // Defaults to undefined
duration: 1500, // Defaults to 1500ms
thresholds: { 30: { color: 'green'}, 50: { color: 'yellow' }, 60: { color: 'red' } }, // Defaults to undefined
labelOnly: false, // Defaults to false
foregroundColor: 'rgba(0, 150, 136, 1)', // Defaults to rgba(0, 150, 136, 1)
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Defaults to rgba(0, 0, 0, 0.1)
fractionSize: 0, // Number of decimal places to round the number to. Defaults to current locale formatting
},
},
]
Well the problem is mostly around design decisions, technically the popup is done. And as a programmer, I’m not very good at design decisions
The typical Tileboard popup (door entry, charts, etc) is just a screen sized container with fixed margins around. So if you start putting in tiles, like I did, they will use absolute positioning, just like with normal Tileboard pages. Basically your popup tile layout will only really work for a specific screen size. Then again, that’s how Tileboard as a whole works too. I was thinking about ways to dynamically adjust popup margins and positioning according to tilesize (so that the popup edges always fit the tiles). But that’s kinda against the entire spirit of TB I guess ? I don’t know. Maybe I should just leave it as-is and let people do their own thing with layouts. The new custom popup uses its own classes, so they can easily be overridden in custom.css
Just had an idea: I could add optional height / width settings for the popup. So the size of each custom popup could easily be modified to fit the contents. Height / width would work like for tiles, as a multiple of tilesize. If they aren’t set for a popup, it will just use the entire screen with fixed margins, as other TB popups do.