feat: Modernize the visual card editor. (#2586)
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 10px 10px 14px;
|
||||
|
||||
border-bottom: 1px solid var(--advanced-camera-card-divider-color);
|
||||
|
||||
text-decoration: none;
|
||||
font-style: italic;
|
||||
|
||||
color: var(--advanced-camera-card-editor-doc-link-color);
|
||||
cursor: help;
|
||||
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: var(--advanced-camera-card-editor-doc-link-hover-background);
|
||||
color: var(--advanced-camera-card-editor-doc-link-hover-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
a ha-icon:first-child {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
a div {
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// The content the editor renders inside an expander body.
|
||||
//
|
||||
// Every editor component that renders such content does so in its own shadow
|
||||
// root, which the editor's own stylesheet cannot reach, so each includes these
|
||||
// rules itself. The theme variables they use need no such treatment: custom
|
||||
// properties inherit across shadow boundaries from the editor element.
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// A single-column grid rather than block layout: the `minmax(0, 1fr)` column
|
||||
// lets children shrink below their intrinsic minimum width, so a form field
|
||||
// with long unbreakable content (an entity ID, a select option) clamps to the
|
||||
// panel width instead of overflowing it horizontally.
|
||||
@mixin shrinkable-column {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
// No background or border of its own: the body is the panel's own surface, so
|
||||
// the margins around the form read as part of it rather than as a frame around
|
||||
// it.
|
||||
.values {
|
||||
@include shrinkable-column;
|
||||
}
|
||||
|
||||
// The blocks stacked in a body all take the same inset, so their outlines line
|
||||
// up with each other and none touches the body edge. It is applied to each
|
||||
// block rather than as body padding because a form's own fields cannot be
|
||||
// reached from outside its shadow DOM, so the form element itself has to carry
|
||||
// the spacing.
|
||||
.values ha-form,
|
||||
.values ha-expansion-panel {
|
||||
display: block;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
// De-emphasized to the same degree Home Assistant de-emphasizes the icons on
|
||||
// the panels `ha-form` renders, so the panels the editor builds itself sit
|
||||
// alongside them without standing out.
|
||||
advanced-camera-card-icon[slot='leading-icon'] {
|
||||
color: var(--advanced-camera-card-editor-secondary-text-color);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// The keyboard shortcuts panel renders editor body content into its own shadow
|
||||
// root, so it carries the shared body rules alongside its own.
|
||||
@use './editor-expander-body.scss';
|
||||
|
||||
// The key assigners are rows of a shared three-column table (label, buttons,
|
||||
// assigned keys): the columns are defined here and each assigner joins them
|
||||
// with a subgrid, so the rows align across components.
|
||||
.values {
|
||||
// `minmax(0, 1fr)` rather than `1fr`: the keys column must be able to
|
||||
// shrink below its content so long key combinations wrap instead of
|
||||
// overflowing.
|
||||
grid-template-columns: max-content max-content minmax(0, 1fr);
|
||||
}
|
||||
|
||||
// Content other than the assigners (the documentation link, the shortcuts
|
||||
// form) spans the whole row.
|
||||
.values > *:not(advanced-camera-card-key-assigner) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// A list of configuration items as rows, styled as Home Assistant styles the
|
||||
// editable lists in its own editors.
|
||||
@use './editor-expander-body.scss';
|
||||
|
||||
// Whatever precedes the list draws a line of its own (a documentation link's
|
||||
// lower border, a form's fields), which the rows' own borders would otherwise
|
||||
// sit directly against.
|
||||
:host {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
gap: 8px;
|
||||
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
border: 1px solid var(--advanced-camera-card-divider-color);
|
||||
border-radius: var(--ha-border-radius-md, 8px);
|
||||
|
||||
--ha-md-list-item-gap: 8px;
|
||||
--md-list-item-top-space: 0;
|
||||
--md-list-item-bottom-space: 0;
|
||||
--md-list-item-leading-space: 12px;
|
||||
--md-list-item-trailing-space: 4px;
|
||||
--md-list-item-one-line-container-height: 48px;
|
||||
--md-list-item-two-line-container-height: 56px;
|
||||
}
|
||||
|
||||
// Sized and de-emphasized as Home Assistant sizes the controls on the rows of
|
||||
// its own editable lists: 36px buttons rather than the 48px default, against a
|
||||
// 48px row.
|
||||
.handle,
|
||||
ha-md-list-item advanced-camera-card-icon[slot='start'] {
|
||||
color: var(--advanced-camera-card-editor-secondary-text-color);
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: move; // Fallback where the grab cursor is unsupported.
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
// The handle is what a drag is started from, so nothing inside it may become
|
||||
// the target of the pointer instead.
|
||||
.handle > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
--ha-icon-button-size: 36px;
|
||||
|
||||
color: var(--advanced-camera-card-editor-secondary-text-color);
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
overflow: hidden;
|
||||
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.add {
|
||||
margin: 8px 10px;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// The editor for one item of a list, shown in place of the list, headed as
|
||||
// Home Assistant heads the editor for a sub-element of a card.
|
||||
@use './editor-expander-body.scss';
|
||||
|
||||
// The shared body rules set `:host { display: block }`, which outranks the
|
||||
// browser's default styling of the `hidden` attribute, so the page is hidden
|
||||
// explicitly. A page is hidden while it stays mounted under a page opened on
|
||||
// top of it, keeping its expanded panels expanded for when it returns.
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
margin: 8px 10px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
overflow: hidden;
|
||||
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
font-size: var(--ha-font-size-l, 1.125rem);
|
||||
}
|
||||
|
||||
// The page's blocks (the item's forms, then any list of its own) are spaced
|
||||
// as Home Assistant spaces the blocks within a form, so a list below a form
|
||||
// reads at the same distance as one form group below another.
|
||||
::slotted(*:not(:last-child)) {
|
||||
display: block;
|
||||
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// A section renders its own panel, so it carries the rules for the panel's
|
||||
// frame and body. The editor around it positions the sections themselves.
|
||||
@use './editor-expander-body.scss';
|
||||
|
||||
// Content the section was given ends its body; without this it sits flush
|
||||
// against the section's bottom edge.
|
||||
.values > advanced-camera-card-editor-cameras,
|
||||
.values > advanced-camera-card-editor-folders,
|
||||
.values > advanced-camera-card-editor-keyboard-shortcuts {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
+22
-133
@@ -1,147 +1,36 @@
|
||||
@use './themes/base.scss';
|
||||
@use './button.scss';
|
||||
|
||||
.option {
|
||||
padding: 8px 4px;
|
||||
cursor: pointer;
|
||||
// A hidden form rendered only to pre-load `ha-form`/`ha-selector`'s lazily
|
||||
// imported element types.
|
||||
.selector-sideload {
|
||||
display: none;
|
||||
}
|
||||
.option.option-overrides .title {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
margin-bottom: -14px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.title {
|
||||
padding-left: 16px;
|
||||
margin-top: -6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.title.warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.secondary {
|
||||
padding-left: 40px;
|
||||
color: var(--secondary-text-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
.values {
|
||||
background: var(--secondary-background-color);
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
// Card-state notice banners sit above the section stack.
|
||||
.card-config > ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
// Space submenus from the next top-level options.
|
||||
.values + .option,
|
||||
.submenu + .option {
|
||||
margin-top: 10px;
|
||||
|
||||
// Each top-level section is an HA expansion panel. An open section draws its
|
||||
// own outline, which would otherwise sit directly against the neighbouring
|
||||
// sections, so every section is spaced whether it is open or not: a gap that
|
||||
// appeared only on opening would move the sections below as the panel grew.
|
||||
// The margins of two adjacent sections collapse together, so the gap is the
|
||||
// same wherever it falls.
|
||||
.section {
|
||||
display: block;
|
||||
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.submenu-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
.submenu.selected > .submenu-header {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.submenu-header * {
|
||||
flex-basis: auto;
|
||||
|
||||
// Only allow clicks on the header.
|
||||
pointer-events: none;
|
||||
}
|
||||
.submenu-header .new {
|
||||
font-style: italic;
|
||||
}
|
||||
.submenu:not(.selected) > .submenu-header .new {
|
||||
color: var(--secondary-text-color, 'black');
|
||||
}
|
||||
|
||||
.submenu-header advanced-camera-card-icon {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.submenu.selected {
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
// Nested menus should shrink width.
|
||||
.submenu {
|
||||
width: calc(100% - 10px * 2);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
// Add a margin above a submenu unless it's another submenu.
|
||||
.submenu:first-child,
|
||||
:not(.submenu) + .submenu {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.submenu .values .controls {
|
||||
display: inline-block;
|
||||
margin-left: auto;
|
||||
margin-right: 0px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.submenu .values .controls ha-icon-button {
|
||||
--ha-icon-button-size: 32px;
|
||||
--mdc-icon-size: calc(var(--ha-icon-button-size) / 2);
|
||||
}
|
||||
span.info {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
ha-selector {
|
||||
padding: 10px;
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.doc-link-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 10px 10px 14px;
|
||||
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
|
||||
text-decoration: none;
|
||||
font-style: italic;
|
||||
|
||||
color: var(--secondary-text-color);
|
||||
cursor: help;
|
||||
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
opacity: 0.7;
|
||||
}
|
||||
.doc-link-row:hover {
|
||||
background-color: var(--secondary-background-color);
|
||||
color: var(--primary-color);
|
||||
opacity: 1;
|
||||
}
|
||||
.doc-link-row ha-icon:first-child {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.doc-link-row div {
|
||||
flex: 1;
|
||||
padding-left: 0;
|
||||
margin-top: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-button {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
+1
-7
@@ -7,19 +7,13 @@
|
||||
}
|
||||
|
||||
ha-icon,
|
||||
state-badge,
|
||||
svg {
|
||||
state-badge {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
svg path {
|
||||
// !important is necessary to overcome the fill set on the path itself.
|
||||
fill: currentColor !important;
|
||||
}
|
||||
|
||||
:host([allow-override-non-active-styles]) state-badge {
|
||||
--state-inactive-color: inherit;
|
||||
}
|
||||
|
||||
+56
-36
@@ -1,12 +1,29 @@
|
||||
// Fade content out over the last `$distance` of the element's width. The
|
||||
// prefixed property covers Safari before 15.4 and Chrome before 120.
|
||||
@mixin fade-out-right($distance: 20px) {
|
||||
$gradient: linear-gradient(
|
||||
to right,
|
||||
black calc(100% - #{$distance}),
|
||||
transparent 100%
|
||||
);
|
||||
|
||||
-webkit-mask-image: $gradient;
|
||||
mask-image: $gradient;
|
||||
}
|
||||
|
||||
// Each assigner is one row of the columns its parent grid defines (label,
|
||||
// buttons, assigned keys), so the rows align like a table across components.
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
align-items: center;
|
||||
|
||||
// Values match other HA editor components.
|
||||
// Values match other HA editor components. A minimum rather than a fixed
|
||||
// height: a long key combination may wrap onto multiple lines.
|
||||
padding: 10px;
|
||||
height: 56px;
|
||||
border: 1px solid var(--divider-color);
|
||||
min-height: 56px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:host([assigning]) ha-button.assign span,
|
||||
@@ -15,7 +32,7 @@
|
||||
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
color: var(--warning-color);
|
||||
color: var(--advanced-camera-card-editor-warning-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,16 +47,27 @@ advanced-camera-card-icon {
|
||||
}
|
||||
|
||||
div.label {
|
||||
width: 100px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
div.key-row {
|
||||
flex: 1;
|
||||
|
||||
div.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.key-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// A long key combination wraps rather than overflowing the row. When even a
|
||||
// single key is wider than the row (a very narrow editor), wrapping cannot
|
||||
// help, so the overflow is clipped and faded out at the edge rather than
|
||||
// spilling out of the panel.
|
||||
flex-wrap: wrap;
|
||||
row-gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@include fade-out-right;
|
||||
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
@@ -49,7 +77,6 @@ div.key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 90%;
|
||||
width: min-content;
|
||||
|
||||
margin-left: 5px;
|
||||
@@ -57,32 +84,25 @@ div.key {
|
||||
}
|
||||
|
||||
div.key-inner {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
|
||||
border: 2px;
|
||||
border-radius: 4px;
|
||||
border-style: outset;
|
||||
border-color: var(--divider-color);
|
||||
|
||||
font-family: monospace;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
div.unassigned {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
div.key + div.key:before {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin-right: 5px;
|
||||
min-width: 24px;
|
||||
|
||||
content: ' + ';
|
||||
padding: 4px 8px;
|
||||
|
||||
background-color: var(--secondary-background-color);
|
||||
border: 1px solid var(--advanced-camera-card-divider-color);
|
||||
border-radius: 4px;
|
||||
|
||||
// The darker lower edge reads as the side of a raised keycap.
|
||||
box-shadow: var(
|
||||
--advanced-camera-card-box-shadow-override,
|
||||
inset 0 -2px 0 var(--advanced-camera-card-divider-color)
|
||||
);
|
||||
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
+15
-1
@@ -9,6 +9,10 @@
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
// The rotating icon's bounding box oscillates as it turns; clip it here so
|
||||
// it cannot grow ancestor scrollable overflow (flickering scrollbars).
|
||||
overflow: hidden;
|
||||
|
||||
background-color: var(--advanced-camera-card-loading-background-color);
|
||||
color: var(--advanced-camera-card-loading-foreground-color);
|
||||
|
||||
@@ -20,10 +24,20 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// The hidden element stays mounted: without this the icon would rotate
|
||||
// invisibly forever.
|
||||
:host([loaded]) advanced-camera-card-icon {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
advanced-camera-card-icon {
|
||||
// The icon's own box sets the size; the inner `ha-icon` machinery sizes
|
||||
// itself from `--mdc-icon-size`, which needs a definite box to fill.
|
||||
--mdc-icon-size: 100%;
|
||||
|
||||
margin-bottom: 20px;
|
||||
height: 25%;
|
||||
width: auto;
|
||||
aspect-ratio: 1;
|
||||
animation: rotate 8s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@@ -301,6 +301,22 @@
|
||||
transparent 20%
|
||||
);
|
||||
|
||||
/********
|
||||
* Editor
|
||||
********/
|
||||
--advanced-camera-card-editor-doc-link-color: var(--secondary-text-color);
|
||||
--advanced-camera-card-editor-doc-link-hover-color: var(
|
||||
--advanced-camera-card-foreground-primary
|
||||
);
|
||||
--advanced-camera-card-editor-doc-link-hover-background: var(
|
||||
--secondary-background-color
|
||||
);
|
||||
--advanced-camera-card-editor-secondary-text-color: var(--secondary-text-color);
|
||||
|
||||
// Home Assistant's amber `--warning-color`, not the card's own
|
||||
// `--advanced-camera-card-warning-color`.
|
||||
--advanced-camera-card-editor-warning-color: var(--warning-color);
|
||||
|
||||
/**************
|
||||
* Notification (block)
|
||||
**************/
|
||||
|
||||
Reference in New Issue
Block a user