mirror of https://github.com/hashicorp/consul
Remove shadow-template/host and related components
parent
b34244c62d
commit
6a4cd42a2b
|
@ -1,87 +0,0 @@
|
||||||
# CustomElement
|
|
||||||
|
|
||||||
A renderless component to aid with the creation of HTML custom elements a.k.a
|
|
||||||
WebComponents.
|
|
||||||
|
|
||||||
All of the CustomElement component arguments are only used at construction
|
|
||||||
time (within the components constructor) therefore they are, by design, static.
|
|
||||||
You shouldn't be dynamically updating these values at all. They are only for
|
|
||||||
type checking and documention purposes and therefore once defined/hardcoded
|
|
||||||
they should only change if you as the author wishes to change them.
|
|
||||||
|
|
||||||
The component is built from various other components, also see their documentaton
|
|
||||||
for further details (`<ShadowHost />`, `<ShadowTemplate />`).
|
|
||||||
|
|
||||||
```hbs preview-template
|
|
||||||
<CustomElement
|
|
||||||
@element="x-component"
|
|
||||||
@attrs={{array
|
|
||||||
(array 'type' '"awesome" | "sauce"' 'awesome' 'Set the type of the x-component')
|
|
||||||
(array 'x' 'number' 0 'The x-ness of the x-component')
|
|
||||||
}}
|
|
||||||
@cssprops={{array
|
|
||||||
(array '--awesome-x-sauce' 'length' '[x]' 'Makes the x-ness of the sauce available to CSS, automatically synced/tracked from the x attributes')
|
|
||||||
(array '--awesome-color' 'color' undefined 'This CSS property can be used to set the color of the awesome')
|
|
||||||
}}
|
|
||||||
@cssparts={{array
|
|
||||||
(array 'base' 'Style base from The Outside via ::part(base)')
|
|
||||||
}}
|
|
||||||
@slots={{array
|
|
||||||
(array 'header' "You'll want to document the slots also")
|
|
||||||
(array '' 'Including the default slot')
|
|
||||||
}}
|
|
||||||
as |custom element|>
|
|
||||||
<x-component
|
|
||||||
{{did-insert custom.connect}}
|
|
||||||
{{will-destroy custom.disconnect}}
|
|
||||||
aria-hidden="true"
|
|
||||||
...attributes
|
|
||||||
>
|
|
||||||
<custom.Template
|
|
||||||
@styles={{css-map
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div part="base"
|
|
||||||
data-x={{element.attrs.x}}
|
|
||||||
data-type={{element.attrs.type}}
|
|
||||||
>
|
|
||||||
<slot name="header"></slot>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</custom.Template>
|
|
||||||
</x-component>
|
|
||||||
</CustomElement>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Arguments
|
|
||||||
|
|
||||||
All `descriptions` in attributes will be compiled out at build time as well as the `@description` attribute itself.
|
|
||||||
|
|
||||||
| Attribute | Type | Default | Description |
|
|
||||||
| :------------ | :------------- | :------ | :------------------------------------------------------------------------- |
|
|
||||||
| element | string | | The custom tag to be used for the custom element. Must include a dash |
|
|
||||||
| description | string | | Short 1 line description for the element. Think "git commit title" style |
|
|
||||||
| attrs | attrInfo[] | | An array of attributes that can be used on the element |
|
|
||||||
| slots | slotsInfo[] | | An array of slots that can be used for the element (100% compiled out) |
|
|
||||||
| cssprops | cssPropsInfo[] | | An array of CSS properties that are relevant to the component |
|
|
||||||
| cssparts | cssPartsInfo[] | | An array of CSS parts that can be used for the element (100% compiled out) |
|
|
||||||
| args | argsInfo[] | | An array of Glimmer arguments used for the component (100% compiled out) |
|
|
||||||
|
|
||||||
## Exports
|
|
||||||
|
|
||||||
### custom
|
|
||||||
|
|
||||||
| Attribute | Type | Description |
|
|
||||||
| :--------- | :------- | :---------------------------------------------------------------------------------- |
|
|
||||||
| connect | function | A did-insert-able callback for tagging an element to be used for the custom element |
|
|
||||||
| disconnect | function | A will-destroy-able callback for destroying an element used for the custom element |
|
|
||||||
|
|
||||||
### element
|
|
||||||
|
|
||||||
| Attribute | Type | Description |
|
|
||||||
| :--------- | :------- | :------------------------------------------------------------------------------- |
|
|
||||||
| attributes | object | An object containing a reference to all the custom elements' observed properties |
|
|
||||||
| * | | All other properties proxy through to the CustomElements class |
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<ShadowHost as |shadow|>
|
|
||||||
{{yield
|
|
||||||
(hash
|
|
||||||
root=(fn this.setHost (fn shadow.host))
|
|
||||||
connect=(fn this.setHost (fn shadow.host))
|
|
||||||
Template=shadow.Template
|
|
||||||
disconnect=(fn this.disconnect)
|
|
||||||
)
|
|
||||||
this.element
|
|
||||||
}}
|
|
||||||
</ShadowHost>
|
|
|
@ -1,189 +0,0 @@
|
||||||
import Component from '@glimmer/component';
|
|
||||||
import { action } from '@ember/object';
|
|
||||||
import { tracked } from '@glimmer/tracking';
|
|
||||||
import { assert } from '@ember/debug';
|
|
||||||
|
|
||||||
const ATTRIBUTE_CHANGE = 'custom-element.attributeChange';
|
|
||||||
const elements = new Map();
|
|
||||||
const proxies = new WeakMap();
|
|
||||||
|
|
||||||
const typeCast = (attributeInfo, value) => {
|
|
||||||
let type = attributeInfo.type;
|
|
||||||
const d = attributeInfo.default;
|
|
||||||
value = value == null ? attributeInfo.default : value;
|
|
||||||
if (type.indexOf('|') !== -1) {
|
|
||||||
assert(
|
|
||||||
`"${value} is not of type '${type}'"`,
|
|
||||||
type
|
|
||||||
.split('|')
|
|
||||||
.map((item) => item.replaceAll('"', '').trim())
|
|
||||||
.includes(value)
|
|
||||||
);
|
|
||||||
type = 'string';
|
|
||||||
}
|
|
||||||
switch (type) {
|
|
||||||
case '<length>':
|
|
||||||
case '<percentage>':
|
|
||||||
case '<dimension>':
|
|
||||||
case 'number': {
|
|
||||||
const num = parseFloat(value);
|
|
||||||
if (isNaN(num)) {
|
|
||||||
return typeof d === 'undefined' ? 0 : d;
|
|
||||||
} else {
|
|
||||||
return num;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case '<integer>':
|
|
||||||
return parseInt(value);
|
|
||||||
case '<string>':
|
|
||||||
case 'string':
|
|
||||||
return (value || '').toString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const attributeChangingElement = (name, Cls = HTMLElement, attributes = {}, cssprops = {}) => {
|
|
||||||
const attrs = Object.keys(attributes);
|
|
||||||
|
|
||||||
const customClass = class extends Cls {
|
|
||||||
static get observedAttributes() {
|
|
||||||
return attrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
|
||||||
const prev = typeCast(attributes[name], oldValue);
|
|
||||||
const value = typeCast(attributes[name], newValue);
|
|
||||||
|
|
||||||
const cssProp = cssprops[`--${name}`];
|
|
||||||
if (typeof cssProp !== 'undefined' && cssProp.track === `[${name}]`) {
|
|
||||||
this.style.setProperty(`--${name}`, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof super.attributeChangedCallback === 'function') {
|
|
||||||
super.attributeChangedCallback(name, prev, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent(ATTRIBUTE_CHANGE, {
|
|
||||||
detail: {
|
|
||||||
name: name,
|
|
||||||
previousValue: prev,
|
|
||||||
value: value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
customElements.define(name, customClass);
|
|
||||||
return () => {};
|
|
||||||
};
|
|
||||||
|
|
||||||
const infoFromArray = (arr, keys) => {
|
|
||||||
return (arr || []).reduce((prev, info) => {
|
|
||||||
let key;
|
|
||||||
const obj = {};
|
|
||||||
keys.forEach((item, i) => {
|
|
||||||
if (item === '_') {
|
|
||||||
key = i;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
obj[item] = info[i];
|
|
||||||
});
|
|
||||||
prev[info[key]] = obj;
|
|
||||||
return prev;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
const debounceRAF = (cb, prev) => {
|
|
||||||
if (typeof prev !== 'undefined') {
|
|
||||||
cancelAnimationFrame(prev);
|
|
||||||
}
|
|
||||||
return requestAnimationFrame(cb);
|
|
||||||
};
|
|
||||||
const createElementProxy = ($element, component) => {
|
|
||||||
return new Proxy($element, {
|
|
||||||
get: (target, prop, receiver) => {
|
|
||||||
switch (prop) {
|
|
||||||
case 'attrs':
|
|
||||||
return component.attributes;
|
|
||||||
default:
|
|
||||||
if (typeof target[prop] === 'function') {
|
|
||||||
// need to ensure we use a MultiWeakMap here
|
|
||||||
// if(this.methods.has(prop)) {
|
|
||||||
// return this.methods.get(prop);
|
|
||||||
// }
|
|
||||||
const method = target[prop].bind(target);
|
|
||||||
// this.methods.set(prop, method);
|
|
||||||
return method;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class CustomElementComponent extends Component {
|
|
||||||
@tracked $element;
|
|
||||||
@tracked _attributes = {};
|
|
||||||
|
|
||||||
__attributes;
|
|
||||||
_attchange;
|
|
||||||
|
|
||||||
constructor(owner, args) {
|
|
||||||
super(...arguments);
|
|
||||||
if (!elements.has(args.element)) {
|
|
||||||
const cb = attributeChangingElement(
|
|
||||||
args.element,
|
|
||||||
args.class,
|
|
||||||
infoFromArray(args.attrs, ['_', 'type', 'default', 'description']),
|
|
||||||
infoFromArray(args.cssprops, ['_', 'type', 'track', 'description'])
|
|
||||||
);
|
|
||||||
elements.set(args.element, cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get attributes() {
|
|
||||||
return this._attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
get element() {
|
|
||||||
if (this.$element) {
|
|
||||||
if (proxies.has(this.$element)) {
|
|
||||||
return proxies.get(this.$element);
|
|
||||||
}
|
|
||||||
const proxy = createElementProxy(this.$element, this);
|
|
||||||
proxies.set(this.$element, proxy);
|
|
||||||
return proxy;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setHost(attachShadow, $element) {
|
|
||||||
attachShadow($element);
|
|
||||||
this.$element = $element;
|
|
||||||
this.$element.addEventListener(ATTRIBUTE_CHANGE, this.attributeChange);
|
|
||||||
|
|
||||||
(this.args.attrs || []).forEach((entry) => {
|
|
||||||
const value = $element.getAttribute(entry[0]);
|
|
||||||
$element.attributeChangedCallback(entry[0], value, value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
disconnect() {
|
|
||||||
this.$element.removeEventListener(ATTRIBUTE_CHANGE, this.attributeChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
attributeChange(e) {
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
// currently if one single attribute changes
|
|
||||||
// they all change
|
|
||||||
this.__attributes = {
|
|
||||||
...this.__attributes,
|
|
||||||
[e.detail.name]: e.detail.value,
|
|
||||||
};
|
|
||||||
this._attchange = debounceRAF(() => {
|
|
||||||
// tell glimmer we changed the attrs
|
|
||||||
this._attributes = this.__attributes;
|
|
||||||
}, this._attchange);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
<!-- START component-docs:@tagName -->
|
|
||||||
# DisclosureCard
|
|
||||||
<!-- END component-docs:@tagName -->
|
|
||||||
|
|
||||||
```hbs preview-template
|
|
||||||
|
|
||||||
<figure>
|
|
||||||
|
|
||||||
<figcaption>
|
|
||||||
Use the component
|
|
||||||
</figcaption>
|
|
||||||
|
|
||||||
<DisclosureCard as |disclosure|>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<h4>api-service-1</h4>
|
|
||||||
</header>
|
|
||||||
<Consul::Bucket::List
|
|
||||||
@item={{hash
|
|
||||||
Namespace="different-nspace"
|
|
||||||
Partition="different-partition"
|
|
||||||
}}
|
|
||||||
@nspace={{'nspace'}}
|
|
||||||
@partition={{'partition'}}
|
|
||||||
/>
|
|
||||||
<DistributionMeter
|
|
||||||
type="linear"
|
|
||||||
as |meter|>
|
|
||||||
<meter.Meter class="warning" percentage="50" />
|
|
||||||
<meter.Meter class="critical" percentage="30" />
|
|
||||||
</DistributionMeter>
|
|
||||||
|
|
||||||
<disclosure.Details
|
|
||||||
|
|
||||||
as |details|>
|
|
||||||
<Consul::Bucket::List
|
|
||||||
@item={{hash
|
|
||||||
Namespace="different-nspace"
|
|
||||||
Partition="different-partition"
|
|
||||||
}}
|
|
||||||
@nspace={{'nspace'}}
|
|
||||||
@partition={{'partition'}}
|
|
||||||
/>
|
|
||||||
<DistributionMeter
|
|
||||||
type="linear"
|
|
||||||
as |meter|>
|
|
||||||
<meter.Meter class="warning" percentage="80" />
|
|
||||||
<meter.Meter class="critical" percentage="10" />
|
|
||||||
</DistributionMeter>
|
|
||||||
|
|
||||||
<Consul::Bucket::List
|
|
||||||
@item={{hash
|
|
||||||
Namespace="different-nspace"
|
|
||||||
Partition="different-partition"
|
|
||||||
}}
|
|
||||||
@nspace={{'nspace'}}
|
|
||||||
@partition={{'partition'}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DistributionMeter
|
|
||||||
type="linear"
|
|
||||||
as |meter|>
|
|
||||||
<meter.Meter class="warning" percentage="10" />
|
|
||||||
<meter.Meter class="critical" percentage="40" />
|
|
||||||
</DistributionMeter>
|
|
||||||
|
|
||||||
<Consul::Bucket::List
|
|
||||||
@item={{hash
|
|
||||||
Namespace="different-nspace"
|
|
||||||
Partition="different-partition"
|
|
||||||
}}
|
|
||||||
@nspace={{'nspace'}}
|
|
||||||
@partition={{'partition'}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DistributionMeter
|
|
||||||
type="linear"
|
|
||||||
as |meter|>
|
|
||||||
<meter.Meter class="warning" percentage="50" />
|
|
||||||
<meter.Meter class="critical" percentage="30" />
|
|
||||||
</DistributionMeter>
|
|
||||||
|
|
||||||
</disclosure.Details>
|
|
||||||
|
|
||||||
<disclosure.Action
|
|
||||||
slot="action"
|
|
||||||
{{on 'click' disclosure.toggle}}
|
|
||||||
>
|
|
||||||
{{if disclosure.expanded "View less" "View more"}}
|
|
||||||
</disclosure.Action>
|
|
||||||
|
|
||||||
</DisclosureCard>
|
|
||||||
|
|
||||||
</figure>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Attributes
|
|
||||||
|
|
||||||
<!-- START component-docs:@attrs -->
|
|
||||||
<!-- END component-docs:@attrs -->
|
|
||||||
|
|
||||||
## Arguments
|
|
||||||
|
|
||||||
<!-- START component-docs:@args -->
|
|
||||||
<!-- END component-docs:@args -->
|
|
||||||
|
|
||||||
## Slots
|
|
||||||
|
|
||||||
<!-- START component-docs:@slots -->
|
|
||||||
<!-- END component-docs:@slots -->
|
|
||||||
|
|
||||||
## CSS Parts
|
|
||||||
|
|
||||||
<!-- START component-docs:@cssparts -->
|
|
||||||
<!-- END component-docs:@cssparts -->
|
|
||||||
|
|
||||||
## CSS Properties
|
|
||||||
|
|
||||||
<!-- START component-docs:@cssprops -->
|
|
||||||
<!-- END component-docs:@cssprops -->
|
|
||||||
|
|
||||||
## Contextual Components
|
|
||||||
|
|
||||||
<!-- START component-docs:@components -->
|
|
||||||
<!-- END component-docs:@components -->
|
|
|
@ -1,90 +0,0 @@
|
||||||
<CustomElement
|
|
||||||
@element="disclosure-card"
|
|
||||||
@description="Block level component with extra disclosable content"
|
|
||||||
@attrs={{array
|
|
||||||
}}
|
|
||||||
as |custom element|>
|
|
||||||
<Disclosure as |disclosure|>
|
|
||||||
<disclosure-card
|
|
||||||
{{did-insert custom.connect}}
|
|
||||||
{{will-destroy custom.disconnect}}
|
|
||||||
expanded={{disclosure.expanded}}
|
|
||||||
>
|
|
||||||
|
|
||||||
<custom.Template
|
|
||||||
@styles={{css-map
|
|
||||||
(require '/styles/base/icons/base-keyframes.css' from='/components/disclosure-card')
|
|
||||||
(require '/styles/base/icons/icons/chevron-down/index.css' from='/components/disclosure-card')
|
|
||||||
(require '/components/panel/index.css' from='/components/disclosure-card')
|
|
||||||
(css "
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
slot[name='action']::slotted(button) {
|
|
||||||
display: block;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
background-color: rgb(var(--tone-gray-050));
|
|
||||||
color: rgb(var(--tone-gray-800));
|
|
||||||
padding-top: var(--padding-y);
|
|
||||||
padding-bottom: var(--padding-y);
|
|
||||||
}
|
|
||||||
slot[name='action']::slotted(button)::after {
|
|
||||||
transition-timing-function: linear;
|
|
||||||
transition-duration: 300ms;
|
|
||||||
transition-property: transform;
|
|
||||||
--icon-name: icon-chevron-down;
|
|
||||||
--icon-size: icon-000;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([expanded]) slot[name='action']::slotted(button)::after {
|
|
||||||
transform: scaleY(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([expanded]) [style*='max-height'] {
|
|
||||||
transition-duration: 50ms;
|
|
||||||
}
|
|
||||||
[style*='max-height'] {
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
transition-property: max-height;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
padding: var(--padding-y) var(--padding-x);
|
|
||||||
}
|
|
||||||
")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div part="base"
|
|
||||||
class={{class-map
|
|
||||||
"panel"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
{{on-resize (dom-position (array
|
|
||||||
(array 'height' 'max-height')
|
|
||||||
))}}
|
|
||||||
class={{class-map
|
|
||||||
'content'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<slot>
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
<hr
|
|
||||||
class={{class-map
|
|
||||||
'panel-separator'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<slot name="action">
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
</custom.Template>
|
|
||||||
|
|
||||||
{{yield disclosure}}
|
|
||||||
|
|
||||||
</disclosure-card>
|
|
||||||
</Disclosure>
|
|
||||||
</CustomElement>
|
|
|
@ -1,83 +0,0 @@
|
||||||
---
|
|
||||||
type: custom-element
|
|
||||||
---
|
|
||||||
<!-- START component-docs:@tagName -->
|
|
||||||
# DistributionMeter
|
|
||||||
<!-- END component-docs:@tagName -->
|
|
||||||
|
|
||||||
<!-- START component-docs:@description -->
|
|
||||||
A meter-like component to show a distribution of values.
|
|
||||||
<!-- END component-docs:@description -->
|
|
||||||
|
|
||||||
```hbs preview-template
|
|
||||||
<figure>
|
|
||||||
<figcaption>
|
|
||||||
Provide a widget so we can try switching between all types of meter
|
|
||||||
</figcaption>
|
|
||||||
<select
|
|
||||||
onchange={{action (mut this.type) value="target.value"}}
|
|
||||||
>
|
|
||||||
<option>linear</option>
|
|
||||||
<option>radial</option>
|
|
||||||
<option>circular</option>
|
|
||||||
</select>
|
|
||||||
</figure>
|
|
||||||
<figure>
|
|
||||||
|
|
||||||
<DataSource
|
|
||||||
@src={{uri '/partition/namespace/dc-1/services'}}
|
|
||||||
as |source|>
|
|
||||||
{{#let
|
|
||||||
(group-by "MeshStatus" (or source.data (array)))
|
|
||||||
as |grouped|}}
|
|
||||||
<DistributionMeter type={{or this.type 'linear'}} as |meter|>
|
|
||||||
{{#each (array 'passing' 'warning' 'critical') as |status|}}
|
|
||||||
{{#let
|
|
||||||
(concat (percentage-of (get grouped (concat status '.length')) source.data.length) '%')
|
|
||||||
as |percentage|}}
|
|
||||||
<meter.Meter
|
|
||||||
description={{capitalize status}}
|
|
||||||
percentage={{percentage}}
|
|
||||||
class={{class-map
|
|
||||||
status
|
|
||||||
}}
|
|
||||||
as |meter|></meter.Meter>
|
|
||||||
{{/let}}
|
|
||||||
{{/each}}
|
|
||||||
</DistributionMeter>
|
|
||||||
{{/let}}
|
|
||||||
</DataSource>
|
|
||||||
</figure>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Attributes
|
|
||||||
|
|
||||||
<!-- START component-docs:@attrs -->
|
|
||||||
| Attribute | Type | Default | Description |
|
|
||||||
| :-------- | :--------------------------------- | :------ | :------------------------------------ |
|
|
||||||
| type | "linear" \| "radial" \| "circular" | linear | The type of distribution meter to use |
|
|
||||||
|
|
||||||
<!-- END component-docs:@attrs -->
|
|
||||||
|
|
||||||
## Contextual Components
|
|
||||||
|
|
||||||
<!-- START component-docs:@components -->
|
|
||||||
|
|
||||||
### DistributionMeter::Meter
|
|
||||||
|
|
||||||
#### Attributes
|
|
||||||
|
|
||||||
| Attribute | Type | Default | Description |
|
|
||||||
| :---------- | :----- | :------ | :----------------------------------------- |
|
|
||||||
| percentage | number | 0 | The percentage to be used for the meter |
|
|
||||||
| description | string | | Textual value to describe the meters value |
|
|
||||||
|
|
||||||
|
|
||||||
#### CSS Properties
|
|
||||||
|
|
||||||
| Property | Type | Tracks | Description |
|
|
||||||
| :---------------------- | :--------- | :----------- | :---------------------------------------------------------------- |
|
|
||||||
| --percentage | percentage | [percentage] | Read-only alias of the percentage attribute |
|
|
||||||
| --aggregated-percentage | percentage | | Aggregated percentage of all meters within the distribution meter |
|
|
||||||
|
|
||||||
<!-- END component-docs:@components -->
|
|
|
@ -1,32 +0,0 @@
|
||||||
export default (css) => {
|
|
||||||
return css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
dl {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
:host([type='linear']) {
|
|
||||||
height: 3px;
|
|
||||||
}
|
|
||||||
:host([type='radial']),
|
|
||||||
:host([type='circular']) {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
:host([type='linear']) dl {
|
|
||||||
background-color: currentColor;
|
|
||||||
color: rgb(var(--tone-gray-100));
|
|
||||||
border-radius: var(--decor-radius-999);
|
|
||||||
transition-property: transform;
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
transition-duration: 0.1s;
|
|
||||||
}
|
|
||||||
:host([type='linear']) dl:hover {
|
|
||||||
transform: scaleY(3);
|
|
||||||
box-shadow: var(--decor-elevation-200);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
};
|
|
|
@ -1,30 +0,0 @@
|
||||||
<CustomElement
|
|
||||||
@element="distribution-meter"
|
|
||||||
@description="A meter-like component to show a distribution of values."
|
|
||||||
@attrs={{array
|
|
||||||
(array 'type' '"linear" | "radial" | "circular"' 'linear'
|
|
||||||
'The type of distribution meter to use'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
as |custom element|>
|
|
||||||
<distribution-meter
|
|
||||||
{{did-insert custom.connect}}
|
|
||||||
{{will-destroy custom.disconnect}}
|
|
||||||
...attributes
|
|
||||||
>
|
|
||||||
<custom.Template
|
|
||||||
@styles={{css-map
|
|
||||||
(require './index.css' from='/components/distribution-meter')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<dl>
|
|
||||||
<slot></slot>
|
|
||||||
</dl>
|
|
||||||
</custom.Template>
|
|
||||||
{{yield (hash
|
|
||||||
Meter=(component 'distribution-meter/meter'
|
|
||||||
type=element.attrs.type
|
|
||||||
)
|
|
||||||
)}}
|
|
||||||
</distribution-meter>
|
|
||||||
</CustomElement>
|
|
|
@ -1,29 +0,0 @@
|
||||||
const parseFloatWithDefault = (val, d = 0) => {
|
|
||||||
const num = parseFloat(val);
|
|
||||||
return isNaN(num) ? d : num;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default (Component) => {
|
|
||||||
return class extends Component {
|
|
||||||
attributeChangedCallback(name, prev, value) {
|
|
||||||
const target = this;
|
|
||||||
switch (name) {
|
|
||||||
case 'percentage': {
|
|
||||||
let prevSibling = target;
|
|
||||||
while (prevSibling) {
|
|
||||||
const nextSibling = prevSibling.nextElementSibling;
|
|
||||||
const aggregatedPercentage = nextSibling
|
|
||||||
? parseFloatWithDefault(nextSibling.style.getPropertyValue('--aggregated-percentage'))
|
|
||||||
: 0;
|
|
||||||
const perc =
|
|
||||||
parseFloatWithDefault(prevSibling.getAttribute('percentage')) + aggregatedPercentage;
|
|
||||||
prevSibling.style.setProperty('--aggregated-percentage', perc);
|
|
||||||
prevSibling.setAttribute('aggregated-percentage', perc);
|
|
||||||
prevSibling = prevSibling.previousElementSibling;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,80 +0,0 @@
|
||||||
export default (css) => {
|
|
||||||
return css`
|
|
||||||
/*@import '~/styles/base/decoration/visually-hidden.css';*/
|
|
||||||
|
|
||||||
:host(.critical) {
|
|
||||||
color: rgb(var(--tone-red-500));
|
|
||||||
}
|
|
||||||
:host(.warning) {
|
|
||||||
color: rgb(var(--tone-orange-500));
|
|
||||||
}
|
|
||||||
:host(.passing) {
|
|
||||||
color: rgb(var(--tone-green-500));
|
|
||||||
}
|
|
||||||
|
|
||||||
:host {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
transition-duration: 0.5s;
|
|
||||||
}
|
|
||||||
dt,
|
|
||||||
dd meter {
|
|
||||||
animation-name: visually-hidden;
|
|
||||||
animation-fill-mode: forwards;
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host(.type-linear) {
|
|
||||||
transition-property: width;
|
|
||||||
width: calc(var(--aggregated-percentage) * 1%);
|
|
||||||
height: 100%;
|
|
||||||
background-color: currentColor;
|
|
||||||
border-radius: var(--decor-radius-999);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host svg {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
:host(.type-radial),
|
|
||||||
:host(.type-circular) {
|
|
||||||
transition-property: none;
|
|
||||||
}
|
|
||||||
:host(.type-radial) dd,
|
|
||||||
:host(.type-circular) dd {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
:host(.type-radial) circle,
|
|
||||||
:host(.type-circular) circle {
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
transition-duration: 0.5s;
|
|
||||||
pointer-events: stroke;
|
|
||||||
transition-property: stroke-dashoffset, stroke-width;
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
transform-origin: 50%;
|
|
||||||
fill: transparent;
|
|
||||||
stroke: currentColor;
|
|
||||||
stroke-dasharray: 100, 100;
|
|
||||||
stroke-dashoffset: calc(calc(100 - var(--aggregated-percentage)) * 1px);
|
|
||||||
}
|
|
||||||
:host([aggregated-percentage='100']) circle {
|
|
||||||
stroke-dasharray: 0 !important;
|
|
||||||
}
|
|
||||||
:host([aggregated-percentage='0']) circle {
|
|
||||||
stroke-dasharray: 0, 100 !important;
|
|
||||||
}
|
|
||||||
:host(.type-radial) circle,
|
|
||||||
:host(.type-circular]) svg {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
:host(.type-radial) circle {
|
|
||||||
stroke-width: 32;
|
|
||||||
}
|
|
||||||
:host(.type-circular) circle {
|
|
||||||
stroke-width: 14;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
};
|
|
|
@ -1,64 +0,0 @@
|
||||||
<CustomElement
|
|
||||||
@element="distribution-meter-meter"
|
|
||||||
@class={{require './element'
|
|
||||||
from='/components/distribution-meter/meter'}}
|
|
||||||
|
|
||||||
@attrs={{array
|
|
||||||
(array 'percentage' 'number' 0
|
|
||||||
'The percentage to be used for the meter'
|
|
||||||
)
|
|
||||||
(array 'description' 'string' ''
|
|
||||||
'Textual value to describe the meters value'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
|
|
||||||
@cssprops={{array
|
|
||||||
(array '--percentage' 'percentage' '[percentage]'
|
|
||||||
'Read-only alias of the percentage attribute'
|
|
||||||
)
|
|
||||||
(array '--aggregated-percentage' 'percentage' undefined
|
|
||||||
'Aggregated percentage of all meters within the distribution meter'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
as |custom element|>
|
|
||||||
|
|
||||||
<distribution-meter-meter
|
|
||||||
{{did-insert custom.connect}}
|
|
||||||
{{will-destroy custom.disconnect}}
|
|
||||||
class={{class-map
|
|
||||||
(array (concat 'type-' @type) @type)
|
|
||||||
}}
|
|
||||||
...attributes
|
|
||||||
>
|
|
||||||
<custom.Template
|
|
||||||
@styles={{css-map
|
|
||||||
(require '/styles/base/decoration/visually-hidden.css'
|
|
||||||
from='/components/distribution-meter/meter')
|
|
||||||
(require './index.css'
|
|
||||||
from='/components/distribution-meter/meter')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<dt>{{element.attrs.description}}</dt>
|
|
||||||
<dd aria-label={{concat element.attrs.percentage '%'}}>
|
|
||||||
<meter min="0" max="100" value={{element.attrs.percentage}}>
|
|
||||||
<slot>{{concat element.attrs.percentage '%'}}</slot>
|
|
||||||
</meter>
|
|
||||||
{{#if (or (eq @type 'circular') (eq @type 'radial'))}}
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 32 32"
|
|
||||||
clip-path="circle()"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
r="16"
|
|
||||||
cx="16"
|
|
||||||
cy="16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{{/if}}
|
|
||||||
</dd>
|
|
||||||
</custom.Template>
|
|
||||||
</distribution-meter-meter>
|
|
||||||
|
|
||||||
</CustomElement>
|
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
# ShadowHost
|
|
||||||
|
|
||||||
`ShadowHost` is a small renderless mainly utility component for easily attaching
|
|
||||||
ShadowDOM to any applicable DOM node. It mainly exists to provide a context for
|
|
||||||
passing around a reference to the element to be used for the shadow template,
|
|
||||||
but named appropriately for recognition.
|
|
||||||
|
|
||||||
If you are looking to write a custom element, please use the `CustomElement`
|
|
||||||
component. If you are simply attaching ShadowDOM to a native HTML element then
|
|
||||||
this is the component for you.
|
|
||||||
|
|
||||||
```hbs preview-template
|
|
||||||
<ShadowHost as |shadow|>
|
|
||||||
<div
|
|
||||||
{{did-insert shadow.host}}
|
|
||||||
>
|
|
||||||
<shadow.Template>
|
|
||||||
<p>hi</p>
|
|
||||||
</shadow.Template>
|
|
||||||
</div>
|
|
||||||
</ShadowHost>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Exports
|
|
||||||
|
|
||||||
| Attribute | Type | Description |
|
|
||||||
| :-------- | :---------------------- | :------------------------------------------------------------------------------- |
|
|
||||||
| host | function | A did-insert-able callback for tagging an element to be used for the shadow root |
|
|
||||||
| Template | ShadowTemplateComponent | ShadowTemplate component pre-configured with the shadow host |
|
|
|
@ -1,5 +0,0 @@
|
||||||
{{yield (hash
|
|
||||||
host=(fn this.attachShadow)
|
|
||||||
root=this.shadowRoot
|
|
||||||
Template=(component 'shadow-template' shadowRoot=this.shadowRoot)
|
|
||||||
)}}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import Component from '@glimmer/component';
|
|
||||||
import { action } from '@ember/object';
|
|
||||||
import { tracked } from '@glimmer/tracking';
|
|
||||||
|
|
||||||
export default class ShadowHostComponent extends Component {
|
|
||||||
@tracked shadowRoot;
|
|
||||||
|
|
||||||
@action
|
|
||||||
attachShadow($element) {
|
|
||||||
this.shadowRoot = $element.attachShadow({ mode: 'open' });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,162 +0,0 @@
|
||||||
# ShadowTemplate
|
|
||||||
|
|
||||||
A component to aid creating ShadowDOM based components (when required), heavily
|
|
||||||
inspired by the upcoming Declarative Shadow DOM spec, a new way to implement and
|
|
||||||
use Shadow DOM directly in HTML.
|
|
||||||
|
|
||||||
Instead of passing `shadowroot="open|closed"` as you would with Declarative
|
|
||||||
Shadow DOM we have a `@shadowRoot` argument to which you would pass the actual
|
|
||||||
Shadow DOM element (which itself either open or closed). You can get a reference
|
|
||||||
to this by using the `{{attach-shadow}}` modifier.
|
|
||||||
|
|
||||||
Additionally a `@styles` argument is made available for you to optionally
|
|
||||||
pass completely isolated, scoped, constructable stylesheets to be used for the
|
|
||||||
Shadow DOM tree (you can also continue to use `<style>` within the template
|
|
||||||
itself also if necessary).
|
|
||||||
|
|
||||||
For the moment we'd generally use a standard div element and add Shadow DOM to
|
|
||||||
it, but as shown in the second example, you could also use it to make
|
|
||||||
Glimmerized native custom-elements using Declarative ShadowDOM and
|
|
||||||
Constructable Stylesheets.
|
|
||||||
|
|
||||||
**Important:** As ShadowDOM elements are completely isolated please take care
|
|
||||||
to use the features available (slots/parts etc) to make sure components built in
|
|
||||||
this way can make use of a11y functionality, i.e. any elements having necessary
|
|
||||||
`id` relationships for a11y reasons should be slotted to ensure that the all
|
|
||||||
`id`s remain in the LightDOM. Native form controls such as inputs etc should
|
|
||||||
also be slotted in order to keep them in the LightDOM to ensure that native
|
|
||||||
form functionality continues to work.
|
|
||||||
|
|
||||||
Beside several advantages of isolated DOM/CSS ShadowDOM slots can also be used
|
|
||||||
within conditionals, something which is currently not possible with
|
|
||||||
Glimmer/Ember slots. Mixing Glimmer/Handlebars conditionals with native
|
|
||||||
ShadowDOM slots will give you this additional feature (see truthy conditional in
|
|
||||||
the example below).
|
|
||||||
|
|
||||||
```hbs preview-template
|
|
||||||
<div
|
|
||||||
class={{class-map
|
|
||||||
"component-name"
|
|
||||||
}}
|
|
||||||
...attributes
|
|
||||||
{{attach-shadow (set this 'shadow')}}
|
|
||||||
>
|
|
||||||
<ShadowTemplate
|
|
||||||
@shadowRoot={{this.shadow}}
|
|
||||||
@styles={{css '
|
|
||||||
:host {
|
|
||||||
background-color: rgb(var(--tone-strawberry-500) / 20%);
|
|
||||||
padding: 1rem; /* 16px */
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
color: purple;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
::slotted(header) {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
::slotted(p) {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
header::before {
|
|
||||||
margin-right: 0.375rem; /* 6px */
|
|
||||||
}
|
|
||||||
'}}
|
|
||||||
>
|
|
||||||
<header part="header">
|
|
||||||
<slot name="header">
|
|
||||||
<h1>Default Header</h1>
|
|
||||||
</slot>
|
|
||||||
</header>
|
|
||||||
<!-- Wrap the slot in a conditional -->
|
|
||||||
{{#if true}}
|
|
||||||
<slot name="body">
|
|
||||||
<p>Default Body</p>
|
|
||||||
</slot>
|
|
||||||
{{/if}}
|
|
||||||
<slot>
|
|
||||||
<!-- The default slot -->
|
|
||||||
</slot>
|
|
||||||
</ShadowTemplate>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
```css
|
|
||||||
.component-name::part(header)::before {
|
|
||||||
@extend %with-logo-consul-color-icon, %as-pseudo;
|
|
||||||
width: 2rem; /* 32px */
|
|
||||||
height: 2rem; /* 32px */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Example with a custom element. **Please note:** These must still be instantiated
|
|
||||||
using Glimmer syntax i.e. `<ComponentName />` not `<component-name />` but a
|
|
||||||
`<component-name />` element will be rendered to the DOM instead of a `<div>`.
|
|
||||||
|
|
||||||
```hbs preview-template
|
|
||||||
<component-name
|
|
||||||
...attributes
|
|
||||||
{{attach-shadow (set this 'shadow')}}
|
|
||||||
>
|
|
||||||
<ShadowTemplate
|
|
||||||
@shadowRoot={{this.shadow}}
|
|
||||||
@styles={{css '
|
|
||||||
header {
|
|
||||||
color: purple;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
::slotted(header) {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
::slotted(p) {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
header::before {
|
|
||||||
margin-right: 0.375rem; /* 6px */
|
|
||||||
}
|
|
||||||
'}}
|
|
||||||
>
|
|
||||||
<header part="header">
|
|
||||||
<slot name="header">
|
|
||||||
<h1>Default Header</h1>
|
|
||||||
</slot>
|
|
||||||
</header>
|
|
||||||
{{#if true}}
|
|
||||||
<slot name="body">
|
|
||||||
<p>Default Body</p>
|
|
||||||
</slot>
|
|
||||||
{{/if}}
|
|
||||||
<slot>
|
|
||||||
<!-- The default slot -->
|
|
||||||
</slot>
|
|
||||||
</ShadowTemplate>
|
|
||||||
</component-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
```css
|
|
||||||
component-name::part(header)::before {
|
|
||||||
@extend %with-logo-consul-color-icon, %as-pseudo;
|
|
||||||
width: 2rem; /* 32px */
|
|
||||||
height: 2rem; /* 32px */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
## Arguments
|
|
||||||
|
|
||||||
| Argument | Type | Default | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `shadowRoot` | `ShadowRoot` | | A reference to a shadow root (probably retrived using the `{{attach-shadow}}` modifier |
|
|
||||||
| `styles` | `CSSResultGroup` | | Styles to be adopted by the ShadowRoot |
|
|
|
@ -1,6 +0,0 @@
|
||||||
[id^='docfy-demo-preview-shadow-template'] .component-name::part(header)::before,
|
|
||||||
[id^='docfy-demo-preview-shadow-template'] component-name::part(header)::before {
|
|
||||||
@extend %with-logo-consul-color-icon, %as-pseudo;
|
|
||||||
width: 2rem; /* 32px */
|
|
||||||
height: 2rem; /* 32px */
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
{{#if @shadowRoot}}
|
|
||||||
{{#in-element @shadowRoot}}
|
|
||||||
{{#if @styles}}
|
|
||||||
{{adopt-styles
|
|
||||||
@shadowRoot
|
|
||||||
@styles
|
|
||||||
}}
|
|
||||||
{{/if}}
|
|
||||||
{{yield}}
|
|
||||||
{{/in-element}}
|
|
||||||
{{/if}}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { setModifierManager, capabilities } from '@ember/modifier';
|
|
||||||
|
|
||||||
export default setModifierManager(
|
|
||||||
() => ({
|
|
||||||
capabilities: capabilities('3.13', { disableAutoTracking: true }),
|
|
||||||
|
|
||||||
createModifier() {},
|
|
||||||
|
|
||||||
installModifier(_state, element, { positional: [fn, ...args], named }) {
|
|
||||||
let shadow;
|
|
||||||
try {
|
|
||||||
shadow = element.attachShadow({ mode: 'open' });
|
|
||||||
} catch (e) {
|
|
||||||
// shadow = false;
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
fn(shadow);
|
|
||||||
},
|
|
||||||
updateModifier() {},
|
|
||||||
destroyModifier() {},
|
|
||||||
}),
|
|
||||||
class CustomElementModifier {}
|
|
||||||
);
|
|
|
@ -1,28 +0,0 @@
|
||||||
# attach-shadow
|
|
||||||
|
|
||||||
`{{attach-shadow (set this 'shadow')}}` attaches a `ShadowRoot` to the modified DOM element
|
|
||||||
and pass a reference to that `ShadowRoot` to the setter function.
|
|
||||||
|
|
||||||
|
|
||||||
Please note: This should be used as a utility modifier for when having access
|
|
||||||
to the shadow DOM is handy, not really for building full blown shadow DOM
|
|
||||||
based Web Components.
|
|
||||||
|
|
||||||
```hbs preview-template
|
|
||||||
<div
|
|
||||||
{{attach-shadow (set this 'shadow')}}
|
|
||||||
>
|
|
||||||
{{#if this.shadow}}
|
|
||||||
{{#in-element this.shadow}}
|
|
||||||
<slot name="name"></slot>
|
|
||||||
{{/in-element}}
|
|
||||||
{{/if}}
|
|
||||||
<p slot="name">Hello from the shadows!</p>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Positional Arguments
|
|
||||||
|
|
||||||
| Argument | Type | Default | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `setter` | `function` | | Usually `set` or `mut` or similar |
|
|
|
@ -5,7 +5,6 @@
|
||||||
@import 'consul-ui/components/badge/debug';
|
@import 'consul-ui/components/badge/debug';
|
||||||
@import 'consul-ui/components/panel/debug';
|
@import 'consul-ui/components/panel/debug';
|
||||||
@import 'consul-ui/components/tile/debug';
|
@import 'consul-ui/components/tile/debug';
|
||||||
@import 'consul-ui/components/shadow-template/debug';
|
|
||||||
@import 'consul-ui/components/csv-list/debug';
|
@import 'consul-ui/components/csv-list/debug';
|
||||||
@import 'consul-ui/components/horizontal-kv-list/debug';
|
@import 'consul-ui/components/horizontal-kv-list/debug';
|
||||||
@import 'consul-ui/components/icon-definition/debug';
|
@import 'consul-ui/components/icon-definition/debug';
|
||||||
|
|
Loading…
Reference in New Issue