holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.73k stars 516 forks source link

ReactiveHTML: Document how to apply same styles to custom ReactiveHTML version of Panel widget #5586

Open MarcSkovMadsen opened 1 year ago

MarcSkovMadsen commented 1 year ago

Sometimes I need a custom version of an existing Panel widget. For example to

I would like the custom widget to have the same styling as the native one across designs and templates. I don't know how to do this easily today. Please document.

Example

Here I would like the two widgets to have the same style. But as you can see for example the <select> padding is different. As soon as you interact, you will also see that the border etc is different.

image

import panel as pn
import param
from panel.reactive import ReactiveHTML

pn.extension()

class CustomSelect(ReactiveHTML):
    value = param.String()
    options = param.List()
    size = param.Integer(default=4, bounds=(1,None))

    _stylesheets = pn.widgets.Select._stylesheets

    _template = """<div class="bk-input-group" styles="width:100%;">
    <label>{{ name }}</label><br/>
    <select id="input_el" class="bk-input" style="background-image: none;width:100%;">
     {% for option in options %}
    <option id="option_el" value={{ option }}>{{ option }}</option>
    {% endfor %}
    </select>
    </div>"""

    _scripts = {
        "render": """
self.value()
input_el.size = data.size
input_el.addEventListener("change", () => data.value=input_el.value)
""",
    "value": """
input_el.value=data.value
"""
    }

select = CustomSelect(value="B", options=["A", "B", "C"], size=5, sizing_mode="stretch_width", margin=10, styles={"background": "salmon"}, name="Contracts")
select2 = pn.widgets.Select(value="B", options=["A", "B", "C"], size=5, sizing_mode="stretch_width", margin=10, styles={"background": "salmon"}, name="Contracts")
# print(select2._styles)
print(select2._stylesheets)
pn.Column(select, select.param.value, select2).servable()
emcd commented 3 months ago

I appreciate the refactored ReactiveHTML documentation, but it seems like the issue of applying the Panel native styles to custom ReactiveHTML widgets has not yet been addressed. Is this under active discussion anywhere? I tried using the style-copying technique from the above example (_stylesheets = pn.widgets.Select._stylesheets), but am running into the same issue where the styling is not being correctly applied even though I have no obvious overrides to it. (The browser's element inspector shows that the relevant Bokeh (.bk-) class is referenced on the element, but not all of the relevant styling is picked up.)

emcd commented 3 months ago

@MarcSkovMadsen: I did some more digging on this. It seems that the Bokeh stylesheets are baked into the underlying Bokeh widgets on the HTML+CSS side and are not surfaced via the _stylesheets or stylesheets attributes on the Python side, since those attributes are for extra stylesheets. The Less sources for those stylesheets are available at:

Instead of compiling the Less templates to CSS, I just took a dump of the CSS stylesheets directly with a browser's DOM inspector and had an LLM pretty them for me. Feels hacky to use so much CSS code directly like this, but here they are anyway:

:host {
    --base-font: var(--bokeh-base-font, Helvetica, Arial, sans-serif);
    --mono-font: var(--bokeh-mono-font, monospace);
    --font-size: var(--bokeh-font-size, 12px);
    --line-height: calc(20 / 14);
    --line-height-computed: calc(var(--font-size) * var(--line-height));
    --border-radius: 4px;
    --padding-vertical: 6px;
    --padding-horizontal: 12px;
    --bokeh-top-level: 1000;
}

:host {
    box-sizing: border-box;
    font-family: var(--base-font);
    font-size: var(--font-size);
    line-height: var(--line-height);
}

*, *:before, *:after {
    box-sizing: inherit;
    font-family: inherit;
}

pre, code {
    font-family: var(--mono-font);
    margin: 0;
}
:host {
    --input-min-height: calc(var(--line-height-computed) + 2*var(--padding-vertical) + 2px);
}

.bk-input {
    position: relative;
    display: inline-block;
    width: 100%;
    flex-grow: 1;
    min-height: var(--input-min-height);
    padding: 0 var(--padding-horizontal);
    background-color: #fff;
    border: 1px solid #ccc;
    border-radius: var(--border-radius);
}

.bk-input:focus {
    border-color: #66afe9;
    outline: 0;
    box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
}

.bk-input::placeholder,
.bk-input:-ms-input-placeholder,
.bk-input::-moz-placeholder,
.bk-input::-webkit-input-placeholder {
    color: #999;
    opacity: 1;
}

.bk-input[disabled],
.bk-input.bk-disabled {
    cursor: not-allowed;
    background-color: #eee;
    opacity: 1;
}

.bk-input-container {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
}

.bk-input-container .bk-input-prefix,
.bk-input-container .bk-input-suffix {
    display: flex;
    align-items: center;
    flex: 0 1 0;
    border: 1px solid #ccc;
    border-radius: var(--border-radius);
    padding: 0 var(--padding-horizontal);
    background-color: #e6e6e6;
}

.bk-input-container .bk-input-prefix {
    border-right: none;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

.bk-input-container .bk-input-suffix {
    border-left: none;
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}

.bk-input-container .bk-input {
    flex: 1 0 0;
}

.bk-input-container .bk-input:not(:first-child) {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}

.bk-input-container .bk-input:not(:last-child) {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

input[type=file].bk-input {
    padding-left: 0;
}

input[type=file]::file-selector-button {
    box-sizing: inherit;
    font-family: inherit;
    font-size: inherit;
    line-height: inherit;
}

select:not([multiple]).bk-input,
select:not([size]).bk-input {
    height: auto;
    appearance: none;
    -webkit-appearance: none;
    background-image: url('data:image/svg+xml;utf8,<svg version="1.1" viewBox="0 0 25 20" xmlns="http://www.w3.org/2000/svg"><path d="M 0,0 25,0 12.5,20 Z" fill="black" /></svg>');
    background-position: right 0.5em center;
    background-size: 8px 6px;
    background-repeat: no-repeat;
    padding-right: calc(var(--padding-horizontal) + 8px);
}

option {
    padding: 0;
}

select[multiple].bk-input,
select[size].bk-input,
textarea.bk-input {
    height: auto;
}

.bk-input-group {
    position: relative;
    width: 100%;
    height: 100%;
    display: inline-flex;
    flex-wrap: nowrap;
    align-items: start;
    flex-direction: column;
    white-space: nowrap;
}

.bk-input-group.bk-inline {
    flex-direction: row;
}

.bk-input-group.bk-inline > *:not(:first-child) {
    margin-left: 5px;
}

.bk-input-group > .bk-spin-wrapper {
    display: inherit;
    width: inherit;
    height: inherit;
    position: relative;
    overflow: hidden;
    padding: 0;
    vertical-align: middle;
}

.bk-input-group > .bk-spin-wrapper input {
    padding-right: 20px;
}

.bk-input-group > .bk-spin-wrapper > .bk-spin-btn {
    position: absolute;
    display: block;
    height: 50%;
    min-height: 0;
    min-width: 0;
    width: 30px;
    padding: 0;
    margin: 0;
    right: 0;
    border: none;
    background: none;
    cursor: pointer;
}

.bk-input-group > .bk-spin-wrapper > .bk-spin-btn:before {
    content: "";
    display: inline-block;
    transform: translateY(-50%);
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
}

.bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-up {
    top: 0;
}

.bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-up:before {
    border-bottom: 5px solid black;
}

.bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-up:disabled:before {
    border-bottom-color: grey;
}

.bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-down {
    bottom: 0;
}

.bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-down:before {
    border-top: 5px solid black;
}

.bk-input-group > .bk-spin-wrapper > .bk-spin-btn.bk-spin-btn-down:disabled:before {
    border-top-color: grey;
}

.bk-description {
    position: relative;
    display: inline-block;
    margin-left: 0.25em;
    vertical-align: middle;
    margin-top: -2px;
    cursor: pointer;
}

.bk-description > .bk-icon {
    opacity: 0.5;
    width: 18px;
    height: 18px;
    background-color: gray;
    mask-image: var(--bokeh-icon-help);
    mask-size: contain;
    mask-repeat: no-repeat;
    -webkit-mask-image: var(--bokeh-icon-help);
    -webkit-mask-size: contain;
    -webkit-mask-repeat: no-repeat;
}

label:hover > .bk-description > .bk-icon,
.bk-icon.bk-opaque {
    opacity: 1;
}

Maybe this will help someone until a better mechanism exists and can be properly documented.