Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
MarcSkovMadsen opened this issue Oct 5, 2023 · 2 comments
Labels
reactivehtml type: docs Related to the Panel documentation and examples type: enhancement Minor feature or improvement to an existing feature

Comments

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Oct 5, 2023

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()
@MarcSkovMadsen MarcSkovMadsen added type: enhancement Minor feature or improvement to an existing feature type: docs Related to the Panel documentation and examples labels Oct 5, 2023
@MarcSkovMadsen MarcSkovMadsen changed the title Document how to apply same styles to custom ReactiveHTML version of Panel widget ReactiveHTML: Document how to apply same styles to custom ReactiveHTML version of Panel widget Oct 5, 2023
@emcd
Copy link

emcd commented Jul 16, 2024

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
Copy link

emcd commented Jul 21, 2024

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
reactivehtml type: docs Related to the Panel documentation and examples type: enhancement Minor feature or improvement to an existing feature
Projects
None yet
Development

No branches or pull requests

2 participants