@tartaguti/ui-components 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +12 -0
- package/custom-elements.json +2887 -823
- package/dist/accordion/tt-accordion-item.d.ts.map +1 -1
- package/dist/accordion/tt-accordion-item.js +5 -2
- package/dist/accordion/tt-accordion-item.js.map +1 -1
- package/dist/bento/tt-bento-item.d.ts.map +1 -1
- package/dist/bento/tt-bento-item.js +7 -13
- package/dist/bento/tt-bento-item.js.map +1 -1
- package/dist/docs.d.ts +7 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +15 -0
- package/dist/docs.js.map +1 -0
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/sidebar/tt-menu-group.d.ts.map +1 -1
- package/dist/sidebar/tt-menu-group.js +16 -5
- package/dist/sidebar/tt-menu-group.js.map +1 -1
- package/dist/sidebar/tt-menu-item.d.ts.map +1 -1
- package/dist/sidebar/tt-menu-item.js +24 -11
- package/dist/sidebar/tt-menu-item.js.map +1 -1
- package/dist/sidebar/tt-sidebar-footer.d.ts.map +1 -1
- package/dist/sidebar/tt-sidebar-footer.js +7 -3
- package/dist/sidebar/tt-sidebar-footer.js.map +1 -1
- package/dist/sidebar/tt-sidebar.d.ts +8 -3
- package/dist/sidebar/tt-sidebar.d.ts.map +1 -1
- package/dist/sidebar/tt-sidebar.js +196 -44
- package/dist/sidebar/tt-sidebar.js.map +1 -1
- package/dist/strings.d.ts +64 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +68 -0
- package/dist/strings.js.map +1 -0
- package/dist/tokens.css +30 -6
- package/dist/tt-alert.d.ts.map +1 -1
- package/dist/tt-alert.js +5 -2
- package/dist/tt-alert.js.map +1 -1
- package/dist/tt-auth.d.ts +5 -1
- package/dist/tt-auth.d.ts.map +1 -1
- package/dist/tt-auth.js +72 -10
- package/dist/tt-auth.js.map +1 -1
- package/dist/tt-breadcrumbs.d.ts.map +1 -1
- package/dist/tt-breadcrumbs.js +6 -3
- package/dist/tt-breadcrumbs.js.map +1 -1
- package/dist/tt-button-group.d.ts.map +1 -1
- package/dist/tt-button-group.js +7 -1
- package/dist/tt-button-group.js.map +1 -1
- package/dist/tt-button.d.ts.map +1 -1
- package/dist/tt-button.js +37 -28
- package/dist/tt-button.js.map +1 -1
- package/dist/tt-card.d.ts.map +1 -1
- package/dist/tt-card.js +9 -6
- package/dist/tt-card.js.map +1 -1
- package/dist/tt-chart.d.ts +10 -2
- package/dist/tt-chart.d.ts.map +1 -1
- package/dist/tt-chart.js +167 -29
- package/dist/tt-chart.js.map +1 -1
- package/dist/tt-checkbox.d.ts.map +1 -1
- package/dist/tt-checkbox.js +5 -2
- package/dist/tt-checkbox.js.map +1 -1
- package/dist/tt-code-block.d.ts +4 -5
- package/dist/tt-code-block.d.ts.map +1 -1
- package/dist/tt-code-block.js +53 -17
- package/dist/tt-code-block.js.map +1 -1
- package/dist/tt-combobox.d.ts +14 -0
- package/dist/tt-combobox.d.ts.map +1 -1
- package/dist/tt-combobox.js +234 -31
- package/dist/tt-combobox.js.map +1 -1
- package/dist/tt-company-card.d.ts +18 -0
- package/dist/tt-company-card.d.ts.map +1 -1
- package/dist/tt-company-card.js +73 -4
- package/dist/tt-company-card.js.map +1 -1
- package/dist/tt-comparison-table.d.ts.map +1 -1
- package/dist/tt-comparison-table.js +6 -3
- package/dist/tt-comparison-table.js.map +1 -1
- package/dist/tt-component-doc.d.ts.map +1 -1
- package/dist/tt-component-doc.js +6 -3
- package/dist/tt-component-doc.js.map +1 -1
- package/dist/tt-contact-list.d.ts +17 -2
- package/dist/tt-contact-list.d.ts.map +1 -1
- package/dist/tt-contact-list.js +68 -10
- package/dist/tt-contact-list.js.map +1 -1
- package/dist/tt-copy-button.d.ts.map +1 -1
- package/dist/tt-copy-button.js +6 -3
- package/dist/tt-copy-button.js.map +1 -1
- package/dist/tt-data-table.d.ts +32 -1
- package/dist/tt-data-table.d.ts.map +1 -1
- package/dist/tt-data-table.js +302 -16
- package/dist/tt-data-table.js.map +1 -1
- package/dist/tt-dock-item.d.ts +53 -0
- package/dist/tt-dock-item.d.ts.map +1 -0
- package/dist/tt-dock-item.js +270 -0
- package/dist/tt-dock-item.js.map +1 -0
- package/dist/tt-dock.d.ts +59 -0
- package/dist/tt-dock.d.ts.map +1 -0
- package/dist/tt-dock.js +337 -0
- package/dist/tt-dock.js.map +1 -0
- package/dist/tt-drawer.d.ts +4 -0
- package/dist/tt-drawer.d.ts.map +1 -1
- package/dist/tt-drawer.js +14 -4
- package/dist/tt-drawer.js.map +1 -1
- package/dist/tt-dropdown-menu.d.ts +5 -0
- package/dist/tt-dropdown-menu.d.ts.map +1 -1
- package/dist/tt-dropdown-menu.js +34 -8
- package/dist/tt-dropdown-menu.js.map +1 -1
- package/dist/tt-file-upload.d.ts +35 -0
- package/dist/tt-file-upload.d.ts.map +1 -1
- package/dist/tt-file-upload.js +210 -21
- package/dist/tt-file-upload.js.map +1 -1
- package/dist/tt-filter.d.ts.map +1 -1
- package/dist/tt-filter.js +10 -5
- package/dist/tt-filter.js.map +1 -1
- package/dist/tt-form.d.ts +25 -1
- package/dist/tt-form.d.ts.map +1 -1
- package/dist/tt-form.js +151 -12
- package/dist/tt-form.js.map +1 -1
- package/dist/tt-grid.d.ts +10 -1
- package/dist/tt-grid.d.ts.map +1 -1
- package/dist/tt-grid.js +38 -9
- package/dist/tt-grid.js.map +1 -1
- package/dist/tt-icon.d.ts.map +1 -1
- package/dist/tt-icon.js +5 -0
- package/dist/tt-icon.js.map +1 -1
- package/dist/tt-inbox.d.ts +4 -0
- package/dist/tt-inbox.d.ts.map +1 -1
- package/dist/tt-inbox.js +13 -3
- package/dist/tt-inbox.js.map +1 -1
- package/dist/tt-input.d.ts.map +1 -1
- package/dist/tt-input.js +15 -5
- package/dist/tt-input.js.map +1 -1
- package/dist/tt-key-values.d.ts +2 -0
- package/dist/tt-key-values.d.ts.map +1 -1
- package/dist/tt-key-values.js +45 -2
- package/dist/tt-key-values.js.map +1 -1
- package/dist/tt-layout.d.ts +53 -2
- package/dist/tt-layout.d.ts.map +1 -1
- package/dist/tt-layout.js +233 -27
- package/dist/tt-layout.js.map +1 -1
- package/dist/tt-modal.d.ts +4 -0
- package/dist/tt-modal.d.ts.map +1 -1
- package/dist/tt-modal.js +14 -4
- package/dist/tt-modal.js.map +1 -1
- package/dist/tt-navbar.d.ts +4 -0
- package/dist/tt-navbar.d.ts.map +1 -1
- package/dist/tt-navbar.js +42 -7
- package/dist/tt-navbar.js.map +1 -1
- package/dist/tt-pagination.d.ts +4 -0
- package/dist/tt-pagination.d.ts.map +1 -1
- package/dist/tt-pagination.js +8 -1
- package/dist/tt-pagination.js.map +1 -1
- package/dist/tt-product.d.ts +67 -15
- package/dist/tt-product.d.ts.map +1 -1
- package/dist/tt-product.js +447 -366
- package/dist/tt-product.js.map +1 -1
- package/dist/tt-progress.d.ts +1 -0
- package/dist/tt-progress.d.ts.map +1 -1
- package/dist/tt-progress.js +26 -7
- package/dist/tt-progress.js.map +1 -1
- package/dist/tt-quantity.d.ts.map +1 -1
- package/dist/tt-quantity.js +5 -2
- package/dist/tt-quantity.js.map +1 -1
- package/dist/tt-radio-group.d.ts.map +1 -1
- package/dist/tt-radio-group.js +5 -2
- package/dist/tt-radio-group.js.map +1 -1
- package/dist/tt-scroller.d.ts +76 -0
- package/dist/tt-scroller.d.ts.map +1 -0
- package/dist/tt-scroller.js +525 -0
- package/dist/tt-scroller.js.map +1 -0
- package/dist/tt-search.d.ts.map +1 -1
- package/dist/tt-search.js +10 -5
- package/dist/tt-search.js.map +1 -1
- package/dist/tt-select.d.ts +9 -0
- package/dist/tt-select.d.ts.map +1 -1
- package/dist/tt-select.js +99 -14
- package/dist/tt-select.js.map +1 -1
- package/dist/tt-slider.d.ts.map +1 -1
- package/dist/tt-slider.js +13 -2
- package/dist/tt-slider.js.map +1 -1
- package/dist/tt-social-links.d.ts.map +1 -1
- package/dist/tt-social-links.js +7 -4
- package/dist/tt-social-links.js.map +1 -1
- package/dist/tt-switcher.d.ts +2 -0
- package/dist/tt-switcher.d.ts.map +1 -1
- package/dist/tt-switcher.js +24 -76
- package/dist/tt-switcher.js.map +1 -1
- package/dist/tt-table-row.d.ts.map +1 -1
- package/dist/tt-table-row.js +5 -2
- package/dist/tt-table-row.js.map +1 -1
- package/dist/tt-table.d.ts.map +1 -1
- package/dist/tt-table.js +5 -2
- package/dist/tt-table.js.map +1 -1
- package/dist/tt-tabs.d.ts.map +1 -1
- package/dist/tt-tabs.js +5 -2
- package/dist/tt-tabs.js.map +1 -1
- package/dist/tt-tag-list.d.ts.map +1 -1
- package/dist/tt-tag-list.js +6 -3
- package/dist/tt-tag-list.js.map +1 -1
- package/dist/tt-textarea.d.ts.map +1 -1
- package/dist/tt-textarea.js +5 -2
- package/dist/tt-textarea.js.map +1 -1
- package/dist/tt-toast.d.ts +4 -1
- package/dist/tt-toast.d.ts.map +1 -1
- package/dist/tt-toast.js +40 -6
- package/dist/tt-toast.js.map +1 -1
- package/dist/tt-toggle-group.d.ts.map +1 -1
- package/dist/tt-toggle-group.js +7 -1
- package/dist/tt-toggle-group.js.map +1 -1
- package/dist/tt-toggle.d.ts.map +1 -1
- package/dist/tt-toggle.js +14 -9
- package/dist/tt-toggle.js.map +1 -1
- package/package.json +1 -1
- package/vscode.html-custom-data.json +241 -66
package/dist/tt-product.js
CHANGED
|
@@ -5,44 +5,112 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
import { LitElement, html, css, nothing } from "lit";
|
|
8
|
-
import { customElement, property } from "lit/decorators.js";
|
|
9
|
-
import { ifDefined } from "lit/directives/if-defined.js";
|
|
8
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
10
9
|
import { styleMap } from "lit/directives/style-map.js";
|
|
11
10
|
import { srOnly } from "./sr-only.js";
|
|
12
|
-
import {
|
|
13
|
-
import "./tt-card.js";
|
|
11
|
+
import { safeUrl } from "./safe-url.js";
|
|
14
12
|
import "./tt-image.js";
|
|
15
13
|
import "./tt-button.js";
|
|
16
14
|
import "./tt-icon.js";
|
|
15
|
+
/** "1", "4 / 3", "16/9": one number, or two with a slash between. */
|
|
16
|
+
const RATIO = /^\s*\d*\.?\d+\s*(\/\s*\d*\.?\d+\s*)?$/;
|
|
17
|
+
/**
|
|
18
|
+
* A product in a shop: photo, name, price and a button.
|
|
19
|
+
*
|
|
20
|
+
* TWO CHOICES, FOUR RESULTS. `orientation` (a tile for a grid, or a row
|
|
21
|
+
* for a list) and `size` (medium, or small for two across on a phone).
|
|
22
|
+
* Every pairing of the two is designed, so there is no combination that
|
|
23
|
+
* breaks. It had four variants and three layouts, twenty-four pairings,
|
|
24
|
+
* and several of them did: text on a pale photo, a white band under the
|
|
25
|
+
* photo of a tall row, a layout that did nothing in a row.
|
|
26
|
+
*
|
|
27
|
+
* PROPS GIVE THE DEFAULTS, SLOTS ADD TO THEM. `src` draws the photo,
|
|
28
|
+
* `rating` the star, `button-label` the button. The `media`, `meta` and
|
|
29
|
+
* `actions` slots add your own beside them (or over the photo), and
|
|
30
|
+
* clearing the prop drops its default. One rule, so nothing disappears
|
|
31
|
+
* when something is added.
|
|
32
|
+
*
|
|
33
|
+
* ONE TARGET. With `href` the name is a link, or with `clickable` a
|
|
34
|
+
* button, and either one is stretched over the whole card by a
|
|
35
|
+
* pseudo-element, so the card is clickable while the accessibility tree
|
|
36
|
+
* holds ONE link named by the product, not a card-sized link wrapping a
|
|
37
|
+
* button (interactive content inside a link is invalid and announced
|
|
38
|
+
* twice). The action row sits above the stretched link, so its buttons
|
|
39
|
+
* stay separate targets.
|
|
40
|
+
*
|
|
41
|
+
* The card draws its own surface with tt-card's tokens rather than
|
|
42
|
+
* composing tt-card, because the stretched link needs the surface as its
|
|
43
|
+
* positioned box and the focus ring has to go round the whole card.
|
|
44
|
+
*
|
|
45
|
+
* @fires tt-product-action - { value, heading } when the built-in button is pressed.
|
|
46
|
+
* @fires tt-product-open - { value, heading } when a `clickable` card is opened.
|
|
47
|
+
*
|
|
48
|
+
* @slot media - Your own media instead of the photo (a carousel, a video), filling the ratio box. Leave `src` empty.
|
|
49
|
+
* @slot badge - Over the top-left of the photo: Sale, New, Sold out.
|
|
50
|
+
* @slot meta - Beside the rating: tags, stock, delivery.
|
|
51
|
+
* @slot actions - After the built-in button: a wishlist icon, a quantity, your own buttons.
|
|
52
|
+
*/
|
|
17
53
|
let TTProduct = class TTProduct extends LitElement {
|
|
18
54
|
constructor() {
|
|
19
55
|
super(...arguments);
|
|
20
|
-
this.src = "";
|
|
21
|
-
this.ratio = "1";
|
|
22
|
-
this.orientation = "vertical";
|
|
23
|
-
this.variant = "default";
|
|
24
|
-
this.layout = "standard";
|
|
25
56
|
this.heading = "";
|
|
57
|
+
/** Its place in the page outline, as on tt-card. */
|
|
58
|
+
this.headingLevel = 3;
|
|
26
59
|
this.description = "";
|
|
27
60
|
this.descriptionLines = 2;
|
|
28
61
|
this.price = "";
|
|
29
62
|
this.wasPrice = "";
|
|
30
63
|
this.rating = "";
|
|
64
|
+
this.src = "";
|
|
65
|
+
this.ratio = "1";
|
|
66
|
+
/** Identifies the product in its events. */
|
|
31
67
|
this.value = "";
|
|
32
|
-
|
|
68
|
+
/** The product's page. The name becomes a link over the whole card. */
|
|
69
|
+
this.href = "";
|
|
70
|
+
/** Without href: the whole card is a button that emits tt-product-open. */
|
|
71
|
+
this.clickable = false;
|
|
72
|
+
/** The built-in button's text. Empty for no built-in button. */
|
|
33
73
|
this.buttonLabel = "Add to basket";
|
|
74
|
+
this.orientation = "vertical";
|
|
75
|
+
this.size = "medium";
|
|
76
|
+
/** Cannot be bought (sold out): the button is off and the photo greys. Still a link. */
|
|
34
77
|
this.disabled = false;
|
|
35
|
-
|
|
36
|
-
|
|
78
|
+
// From slotchange: CSS cannot ask whether a slot has anything in it.
|
|
79
|
+
this.hasMedia = false;
|
|
80
|
+
this.hasMeta = false;
|
|
81
|
+
this.hasActions = false;
|
|
82
|
+
this.onMedia = (event) => (this.hasMedia = this.filled(event));
|
|
83
|
+
this.onMeta = (event) => (this.hasMeta = this.filled(event));
|
|
84
|
+
this.onActions = (event) => (this.hasActions = this.filled(event));
|
|
85
|
+
}
|
|
86
|
+
filled(event) {
|
|
87
|
+
const slot = event.target;
|
|
88
|
+
return slot
|
|
89
|
+
.assignedNodes({ flatten: true })
|
|
90
|
+
.some((node) => node.nodeType === Node.ELEMENT_NODE || (node.textContent ?? "").trim() !== "");
|
|
91
|
+
}
|
|
92
|
+
get detail() {
|
|
93
|
+
return { value: this.value, heading: this.heading };
|
|
94
|
+
}
|
|
95
|
+
/* The built-in button's own event stays inside: the card reports
|
|
96
|
+
what happened, once, in its own words. A button you slot into
|
|
97
|
+
actions is yours, so its events reach you as usual. */
|
|
98
|
+
handleAction(event) {
|
|
99
|
+
event.stopPropagation();
|
|
37
100
|
if (this.disabled)
|
|
38
101
|
return;
|
|
39
|
-
this.dispatchEvent(new CustomEvent("tt-product-action", {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
102
|
+
this.dispatchEvent(new CustomEvent("tt-product-action", { detail: this.detail, bubbles: true, composed: true }));
|
|
103
|
+
}
|
|
104
|
+
handleOpen() {
|
|
105
|
+
this.dispatchEvent(new CustomEvent("tt-product-open", { detail: this.detail, bubbles: true, composed: true }));
|
|
106
|
+
}
|
|
107
|
+
/** The link's address, when it is one a browser may follow. */
|
|
108
|
+
get url() {
|
|
109
|
+
return this.href && safeUrl(this.href) ? this.href.trim() : "";
|
|
110
|
+
}
|
|
111
|
+
/** A link or a button covers the card: it has a name to carry. */
|
|
112
|
+
get interactive() {
|
|
113
|
+
return this.heading !== "" && (this.url !== "" || this.clickable);
|
|
46
114
|
}
|
|
47
115
|
static { this.styles = [srOnly, css `
|
|
48
116
|
/* The browser's own [hidden] rule loses to any :host display, so a
|
|
@@ -53,509 +121,498 @@ let TTProduct = class TTProduct extends LitElement {
|
|
|
53
121
|
|
|
54
122
|
:host {
|
|
55
123
|
display: block;
|
|
56
|
-
|
|
124
|
+
/* Fills its grid cell, so a row of cards is one height and the
|
|
125
|
+
prices and buttons line up at the bottom. */
|
|
57
126
|
height: 100%;
|
|
58
|
-
|
|
127
|
+
min-width: 0;
|
|
59
128
|
text-align: start;
|
|
129
|
+
font-family: var(--tt-font-sans);
|
|
130
|
+
-webkit-font-smoothing: antialiased;
|
|
60
131
|
|
|
61
|
-
--
|
|
132
|
+
--_radius: var(--tt-radius-lg);
|
|
133
|
+
--_pad: var(--tt-space-4);
|
|
134
|
+
--_title: var(--tt-text-sm);
|
|
62
135
|
--_price: var(--tt-text-md);
|
|
63
|
-
--
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
136
|
+
--_was: var(--tt-text-sm);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* A step down for two across on a phone, where 52px of the row is the
|
|
140
|
+
button and its gap: "EUR 89.00" still fits beside it at 160px. */
|
|
141
|
+
:host([size="small"]) {
|
|
142
|
+
--_pad: var(--tt-space-3);
|
|
143
|
+
--_price: var(--tt-text-sm);
|
|
144
|
+
--_was: var(--tt-text-xs);
|
|
68
145
|
}
|
|
69
146
|
|
|
70
147
|
:host([orientation="horizontal"]) {
|
|
71
|
-
--
|
|
148
|
+
--_title: var(--tt-text-md);
|
|
72
149
|
--_price: var(--tt-text-lg);
|
|
73
150
|
}
|
|
74
151
|
|
|
75
|
-
:host([
|
|
76
|
-
--
|
|
77
|
-
--
|
|
78
|
-
--
|
|
152
|
+
:host([orientation="horizontal"][size="small"]) {
|
|
153
|
+
--_title: var(--tt-text-sm);
|
|
154
|
+
--_price: var(--tt-text-md);
|
|
155
|
+
--_was: var(--tt-text-sm);
|
|
79
156
|
}
|
|
80
157
|
|
|
81
|
-
|
|
82
|
-
|
|
158
|
+
[hidden] {
|
|
159
|
+
display: none !important;
|
|
83
160
|
}
|
|
84
161
|
|
|
85
|
-
:
|
|
86
|
-
--_card-radius: var(--tt-radius-xl);
|
|
87
|
-
--_shadow: var(--tt-elevation-1);
|
|
88
|
-
}
|
|
162
|
+
/* ---------- the surface: tt-card's tokens ---------- */
|
|
89
163
|
|
|
90
|
-
|
|
164
|
+
.card {
|
|
165
|
+
/* The stretched link's box. Nothing between it and the link may be
|
|
166
|
+
positioned, or the link would cover that instead. */
|
|
167
|
+
position: relative;
|
|
168
|
+
box-sizing: border-box;
|
|
91
169
|
display: flex;
|
|
92
170
|
flex-direction: column;
|
|
93
171
|
height: 100%;
|
|
94
|
-
|
|
95
|
-
--tt-radius-nested follow the product's radius. */
|
|
96
|
-
--tt-card-radius: var(--_card-radius);
|
|
97
|
-
transition: transform var(--tt-duration-normal) var(--tt-ease-out);
|
|
98
|
-
}
|
|
172
|
+
min-width: 0;
|
|
99
173
|
|
|
100
|
-
|
|
101
|
-
|
|
174
|
+
background: var(--tt-card);
|
|
175
|
+
color: var(--tt-card-foreground);
|
|
176
|
+
border: 1px solid var(--tt-border);
|
|
177
|
+
border-radius: var(--_radius);
|
|
178
|
+
box-shadow: var(--tt-elevation-1);
|
|
102
179
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
looked the same: "minimal" still drew a white card with a border.
|
|
107
|
-
::part() is the one door a component opens for exactly this.
|
|
108
|
-
*/
|
|
109
|
-
tt-card::part(card) {
|
|
110
|
-
background: var(--_surface);
|
|
111
|
-
border-color: var(--_border);
|
|
112
|
-
box-shadow: var(--_shadow);
|
|
113
|
-
overflow: hidden; /* clips the image to the card's corners */
|
|
180
|
+
transition:
|
|
181
|
+
border-color var(--tt-duration-normal) var(--tt-ease),
|
|
182
|
+
box-shadow var(--tt-duration-normal) var(--tt-ease);
|
|
114
183
|
}
|
|
115
184
|
|
|
116
|
-
:host(
|
|
117
|
-
|
|
185
|
+
:host([orientation="horizontal"]) .card {
|
|
186
|
+
flex-direction: row;
|
|
118
187
|
}
|
|
119
188
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
189
|
+
/* Hover promises a click, so only a card that has one lifts. Only
|
|
190
|
+
where a pointer can hover: a tap would leave it on. */
|
|
191
|
+
@media (hover: hover) {
|
|
192
|
+
.interactive:hover {
|
|
193
|
+
border-color: var(--tt-border-strong);
|
|
194
|
+
box-shadow: var(--tt-elevation-2);
|
|
195
|
+
}
|
|
123
196
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
197
|
+
.interactive:hover .image {
|
|
198
|
+
transform: scale(1.03);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.interactive:hover .link {
|
|
202
|
+
text-decoration: underline;
|
|
203
|
+
text-underline-offset: 0.2em;
|
|
204
|
+
}
|
|
128
205
|
}
|
|
129
206
|
|
|
130
|
-
/* ---------- media
|
|
207
|
+
/* ---------- media ---------- */
|
|
131
208
|
|
|
132
209
|
.media {
|
|
133
210
|
position: relative;
|
|
211
|
+
flex: none;
|
|
134
212
|
overflow: hidden;
|
|
135
213
|
background: var(--tt-muted);
|
|
136
|
-
|
|
214
|
+
|
|
215
|
+
/* The card's curve less its border, on the corners it shares. */
|
|
216
|
+
--_inner: max(0px, calc(var(--_radius) - 1px));
|
|
217
|
+
border-start-start-radius: var(--_inner);
|
|
218
|
+
border-start-end-radius: var(--_inner);
|
|
137
219
|
}
|
|
138
220
|
|
|
139
|
-
|
|
221
|
+
/* The ratio box: an in-flow spacer sets the height from the width.
|
|
222
|
+
The photo is laid over it, so in a row whose text is taller the
|
|
223
|
+
media stretches and the photo still covers it, with no band of
|
|
224
|
+
card showing under it. */
|
|
225
|
+
.media::before {
|
|
226
|
+
content: "";
|
|
140
227
|
display: block;
|
|
228
|
+
aspect-ratio: var(--_ratio, 1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
:host([orientation="horizontal"]) .media {
|
|
232
|
+
flex: 0 0 clamp(6rem, 34%, 13.75rem);
|
|
233
|
+
border-start-end-radius: 0;
|
|
234
|
+
border-end-start-radius: var(--_inner);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
:host([orientation="horizontal"][size="small"]) .media {
|
|
238
|
+
flex-basis: clamp(4.5rem, 26%, 8rem);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.image,
|
|
242
|
+
slot[name="media"]::slotted(*) {
|
|
243
|
+
position: absolute;
|
|
244
|
+
inset: 0;
|
|
141
245
|
width: 100%;
|
|
142
246
|
height: 100%;
|
|
143
|
-
object-fit: cover;
|
|
144
|
-
transition: transform var(--tt-duration-slow) var(--tt-ease-out);
|
|
145
247
|
}
|
|
146
248
|
|
|
147
|
-
|
|
148
|
-
|
|
249
|
+
.image {
|
|
250
|
+
transition: transform var(--tt-duration-slow) var(--tt-ease-out);
|
|
149
251
|
}
|
|
150
252
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
253
|
+
:host([disabled]) .image,
|
|
254
|
+
:host([disabled]) slot[name="media"]::slotted(*) {
|
|
255
|
+
filter: grayscale(1);
|
|
256
|
+
opacity: 0.6;
|
|
155
257
|
}
|
|
156
258
|
|
|
157
|
-
|
|
259
|
+
/* Badges sit on a photo, where a tinted badge (a 13% wash of its
|
|
260
|
+
colour) would show the photo through it. Each one gets the card
|
|
261
|
+
behind it, so it reads exactly as it does on a card. */
|
|
262
|
+
.badges {
|
|
158
263
|
position: absolute;
|
|
264
|
+
inset-block-start: var(--tt-space-3);
|
|
265
|
+
inset-inline: var(--tt-space-3);
|
|
266
|
+
display: flex;
|
|
267
|
+
flex-wrap: wrap;
|
|
268
|
+
gap: var(--tt-space-1);
|
|
269
|
+
pointer-events: none;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
:host([size="small"]) .badges {
|
|
159
273
|
inset-block-start: var(--tt-space-2);
|
|
160
|
-
inset-inline
|
|
161
|
-
z-index: 2;
|
|
162
|
-
max-width: calc(100% - var(--tt-space-4));
|
|
274
|
+
inset-inline: var(--tt-space-2);
|
|
163
275
|
}
|
|
164
276
|
|
|
165
|
-
.
|
|
277
|
+
.badges ::slotted(*) {
|
|
278
|
+
max-width: 100%;
|
|
279
|
+
background: var(--tt-card);
|
|
280
|
+
border-radius: var(--tt-radius-full);
|
|
166
281
|
box-shadow: var(--tt-elevation-1);
|
|
167
282
|
}
|
|
168
283
|
|
|
169
|
-
/* ---------- body
|
|
284
|
+
/* ---------- body ---------- */
|
|
170
285
|
|
|
171
286
|
.body {
|
|
172
287
|
display: flex;
|
|
173
|
-
flex-direction: column;
|
|
174
288
|
flex: 1;
|
|
289
|
+
flex-direction: column;
|
|
290
|
+
gap: var(--tt-space-1);
|
|
175
291
|
min-width: 0;
|
|
292
|
+
padding: var(--_pad);
|
|
176
293
|
}
|
|
177
294
|
|
|
178
|
-
|
|
295
|
+
.heading {
|
|
179
296
|
margin: 0;
|
|
180
297
|
display: -webkit-box;
|
|
181
298
|
-webkit-line-clamp: 2;
|
|
182
299
|
-webkit-box-orient: vertical;
|
|
183
300
|
overflow: hidden;
|
|
184
301
|
|
|
185
|
-
|
|
302
|
+
color: var(--tt-foreground);
|
|
303
|
+
font-size: var(--_title);
|
|
186
304
|
font-weight: var(--tt-weight-semibold);
|
|
187
305
|
line-height: var(--tt-leading-snug);
|
|
188
|
-
letter-spacing: -0.
|
|
189
|
-
color: var(--tt-foreground);
|
|
306
|
+
letter-spacing: -0.01em;
|
|
190
307
|
overflow-wrap: break-word;
|
|
191
308
|
}
|
|
192
309
|
|
|
193
|
-
.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
310
|
+
.link {
|
|
311
|
+
margin: 0;
|
|
312
|
+
padding: 0;
|
|
313
|
+
border: 0;
|
|
314
|
+
background: none;
|
|
315
|
+
color: inherit;
|
|
316
|
+
font: inherit;
|
|
317
|
+
letter-spacing: inherit;
|
|
318
|
+
text-align: inherit;
|
|
319
|
+
text-decoration: none;
|
|
320
|
+
cursor: pointer;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* The whole card is the link's hit area. The ring goes round the
|
|
324
|
+
card, on the same corners. */
|
|
325
|
+
.link::after {
|
|
326
|
+
content: "";
|
|
327
|
+
position: absolute;
|
|
328
|
+
inset: -1px;
|
|
329
|
+
z-index: 1;
|
|
330
|
+
border-radius: var(--_radius);
|
|
331
|
+
}
|
|
199
332
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
letter-spacing: -0.025em;
|
|
203
|
-
color: var(--tt-foreground);
|
|
204
|
-
font-variant-numeric: tabular-nums;
|
|
333
|
+
.link:focus-visible {
|
|
334
|
+
outline: none;
|
|
205
335
|
}
|
|
206
336
|
|
|
207
|
-
.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
text-decoration: line-through;
|
|
211
|
-
color: var(--tt-muted-foreground);
|
|
337
|
+
.link:focus-visible::after {
|
|
338
|
+
outline: var(--tt-ring-width) solid var(--tt-ring);
|
|
339
|
+
outline-offset: var(--tt-ring-offset);
|
|
212
340
|
}
|
|
213
341
|
|
|
214
|
-
|
|
215
|
-
margin:
|
|
342
|
+
.description {
|
|
343
|
+
margin: 0;
|
|
216
344
|
display: -webkit-box;
|
|
217
|
-
-webkit-line-clamp: var(--
|
|
345
|
+
-webkit-line-clamp: var(--_lines, 2);
|
|
218
346
|
-webkit-box-orient: vertical;
|
|
219
347
|
overflow: hidden;
|
|
220
348
|
|
|
349
|
+
color: var(--tt-muted-foreground);
|
|
221
350
|
font-size: var(--tt-text-sm);
|
|
222
351
|
line-height: var(--tt-leading-snug);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.meta {
|
|
355
|
+
display: flex;
|
|
356
|
+
flex-wrap: wrap;
|
|
357
|
+
align-items: center;
|
|
358
|
+
gap: var(--tt-space-1) var(--tt-space-2);
|
|
359
|
+
min-width: 0;
|
|
360
|
+
margin-top: var(--tt-space-1);
|
|
361
|
+
|
|
223
362
|
color: var(--tt-muted-foreground);
|
|
363
|
+
font-size: var(--tt-text-xs);
|
|
364
|
+
line-height: var(--tt-leading-snug);
|
|
224
365
|
}
|
|
225
366
|
|
|
226
367
|
.rating {
|
|
227
368
|
display: inline-flex;
|
|
228
369
|
align-items: center;
|
|
229
370
|
gap: var(--tt-space-1);
|
|
230
|
-
min-width: 0;
|
|
231
|
-
|
|
232
|
-
font-size: var(--tt-text-xs);
|
|
233
|
-
color: var(--tt-muted-foreground);
|
|
234
371
|
font-variant-numeric: tabular-nums;
|
|
235
372
|
}
|
|
236
373
|
|
|
237
374
|
.rating tt-icon {
|
|
375
|
+
--tt-icon-size: 0.875rem;
|
|
238
376
|
color: var(--tt-warning);
|
|
239
|
-
flex: 0 0 auto;
|
|
240
377
|
}
|
|
241
378
|
|
|
242
|
-
/* ----------
|
|
243
|
-
|
|
244
|
-
:host([orientation="vertical"]) .media {
|
|
245
|
-
margin-bottom: var(--tt-space-3);
|
|
246
|
-
border-radius: 0;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
:host([orientation="vertical"]) .price,
|
|
250
|
-
:host([orientation="vertical"]) p {
|
|
251
|
-
margin-top: var(--tt-space-1);
|
|
252
|
-
}
|
|
379
|
+
/* ---------- price and actions, pinned to the bottom ---------- */
|
|
253
380
|
|
|
254
381
|
.foot {
|
|
255
382
|
display: flex;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
383
|
+
flex-direction: column;
|
|
384
|
+
align-items: stretch;
|
|
385
|
+
gap: var(--tt-space-3);
|
|
259
386
|
margin-top: auto;
|
|
260
387
|
padding-top: var(--tt-space-3);
|
|
261
388
|
}
|
|
262
389
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
/* tt-card's padding variables, not padding on its host: host padding
|
|
274
|
-
lands OUTSIDE the card's border and left a gap around it. */
|
|
275
|
-
:host([orientation="vertical"][layout="compact"]) tt-card {
|
|
276
|
-
--tt-card-padding-x: var(--tt-space-2);
|
|
277
|
-
--tt-card-padding-y: var(--tt-space-2);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
:host([orientation="vertical"][layout="compact"]) .media {
|
|
281
|
-
margin-bottom: var(--tt-space-2);
|
|
282
|
-
border-radius: 0;
|
|
390
|
+
/* A row: the price, then the action at the end. Small tiles and
|
|
391
|
+
rows; it wraps under the price when the row is too narrow. */
|
|
392
|
+
:host([size="small"]) .foot,
|
|
393
|
+
:host([orientation="horizontal"]) .foot {
|
|
394
|
+
flex-direction: row;
|
|
395
|
+
flex-wrap: wrap;
|
|
396
|
+
align-items: center;
|
|
397
|
+
justify-content: space-between;
|
|
398
|
+
gap: var(--tt-space-2) var(--tt-space-3);
|
|
283
399
|
}
|
|
284
400
|
|
|
285
|
-
:
|
|
286
|
-
|
|
287
|
-
|
|
401
|
+
/* A small one keeps the icon beside the price: the old price drops
|
|
402
|
+
under the new one instead of pushing the button onto a line of its
|
|
403
|
+
own, so every tile in a row ends the same way. Extra actions of
|
|
404
|
+
your own may need that line, so then it wraps. */
|
|
405
|
+
:host([size="small"]) .foot {
|
|
406
|
+
column-gap: var(--tt-space-2);
|
|
288
407
|
}
|
|
289
408
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
:host([orientation="vertical"][layout="overlay"]) tt-card {
|
|
293
|
-
position: relative;
|
|
294
|
-
--tt-card-padding-x: 0px;
|
|
295
|
-
--tt-card-padding-y: 0px;
|
|
296
|
-
--tt-card-gap: 0px;
|
|
409
|
+
:host([size="small"]) .foot:not(.extra) {
|
|
410
|
+
flex-wrap: nowrap;
|
|
297
411
|
}
|
|
298
412
|
|
|
299
|
-
|
|
413
|
+
.price {
|
|
300
414
|
margin: 0;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
inset-block-end: 0;
|
|
309
|
-
z-index: 2;
|
|
310
|
-
padding: var(--tt-space-4);
|
|
311
|
-
|
|
312
|
-
/*
|
|
313
|
-
Text ON A PHOTO, the one place in the library where colour is
|
|
314
|
-
deliberately not themed: the scrim below is always dark, whatever
|
|
315
|
-
the theme, so the text on it is always light. These literals are
|
|
316
|
-
that decision, written once. (The colour here used to be
|
|
317
|
-
--tt-card-foreground, which is DARK in the light theme: any text
|
|
318
|
-
not overridden below came out dark on the dark scrim.)
|
|
319
|
-
*/
|
|
320
|
-
--_on-image: oklch(1 0 0);
|
|
321
|
-
--_on-image-muted: oklch(1 0 0 / 0.85);
|
|
322
|
-
color: var(--_on-image);
|
|
323
|
-
background: linear-gradient(
|
|
324
|
-
to top,
|
|
325
|
-
oklch(0 0 0 / 0.85) 0%,
|
|
326
|
-
oklch(0 0 0 / 0.45) 60%,
|
|
327
|
-
transparent 100%
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
:host([orientation="vertical"][layout="overlay"]) h2,
|
|
332
|
-
:host([orientation="vertical"][layout="overlay"]) .price {
|
|
333
|
-
color: var(--_on-image);
|
|
415
|
+
min-width: 0;
|
|
416
|
+
color: var(--tt-foreground);
|
|
417
|
+
font-size: var(--_price);
|
|
418
|
+
font-weight: var(--tt-weight-bold);
|
|
419
|
+
line-height: var(--tt-leading-tight);
|
|
420
|
+
letter-spacing: -0.02em;
|
|
421
|
+
font-variant-numeric: tabular-nums;
|
|
334
422
|
}
|
|
335
423
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
424
|
+
/* The old price drops under the new one when there is no room
|
|
425
|
+
beside it: a line can break between two inline blocks, and with no
|
|
426
|
+
space between the spans it could not. Only on a tile too narrow
|
|
427
|
+
for even one price does a price break inside itself, rather than
|
|
428
|
+
run out of the card. The gap is on the end of the new price, so a
|
|
429
|
+
wrapped old price starts flush with it. */
|
|
430
|
+
.now,
|
|
431
|
+
.was {
|
|
432
|
+
display: inline-block;
|
|
339
433
|
}
|
|
340
434
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
.row {
|
|
344
|
-
display: flex;
|
|
345
|
-
align-items: stretch;
|
|
346
|
-
min-height: 7rem;
|
|
347
|
-
width: 100%;
|
|
348
|
-
overflow: hidden;
|
|
349
|
-
border-radius: inherit;
|
|
435
|
+
.now {
|
|
436
|
+
margin-inline-end: var(--tt-space-2);
|
|
350
437
|
}
|
|
351
438
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
439
|
+
/* On sale, the price takes the sale colour; the struck-through old
|
|
440
|
+
price beside it says the same thing without colour. */
|
|
441
|
+
.now.sale {
|
|
442
|
+
color: var(--tt-destructive);
|
|
356
443
|
}
|
|
357
444
|
|
|
358
|
-
.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
445
|
+
.was {
|
|
446
|
+
color: var(--tt-muted-foreground);
|
|
447
|
+
font-size: var(--_was);
|
|
448
|
+
font-weight: var(--tt-weight-regular);
|
|
449
|
+
letter-spacing: 0;
|
|
450
|
+
text-decoration-thickness: 1px;
|
|
364
451
|
}
|
|
365
452
|
|
|
453
|
+
/* Above the stretched link: its buttons are targets of their own. */
|
|
366
454
|
.actions {
|
|
455
|
+
position: relative;
|
|
456
|
+
z-index: 2;
|
|
367
457
|
display: flex;
|
|
458
|
+
flex-wrap: wrap;
|
|
368
459
|
align-items: center;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
padding-top: var(--tt-space-3);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
:host([orientation="horizontal"][layout="compact"]) .row {
|
|
375
|
-
min-height: 5.5rem;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
:host([orientation="horizontal"][layout="compact"]) .column {
|
|
379
|
-
padding: var(--tt-space-3);
|
|
460
|
+
gap: var(--tt-space-2);
|
|
461
|
+
min-width: 0;
|
|
380
462
|
}
|
|
381
463
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
464
|
+
/* A tile's built-in button takes the width; anything added keeps
|
|
465
|
+
its own size beside it. */
|
|
466
|
+
.actions > .action {
|
|
467
|
+
flex: 1 1 auto;
|
|
385
468
|
}
|
|
386
469
|
|
|
387
|
-
|
|
388
|
-
|
|
470
|
+
/* At the end of the row. The built-in button alone is never
|
|
471
|
+
squeezed: the price gives way first, by wrapping. Your own actions
|
|
472
|
+
get a line of their own when they need it, and wrap inside it
|
|
473
|
+
rather than run out of the card. */
|
|
474
|
+
:host([size="small"]) .actions,
|
|
475
|
+
:host([orientation="horizontal"]) .actions {
|
|
476
|
+
justify-content: flex-end;
|
|
477
|
+
margin-inline-start: auto;
|
|
389
478
|
}
|
|
390
479
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
opacity: var(--tt-opacity-disabled);
|
|
395
|
-
pointer-events: none;
|
|
480
|
+
:host([size="small"]) .foot:not(.extra) .actions,
|
|
481
|
+
:host([orientation="horizontal"]) .foot:not(.extra) .actions {
|
|
482
|
+
flex-shrink: 0;
|
|
396
483
|
}
|
|
397
484
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
:host([orientation="horizontal"]) .row .media {
|
|
404
|
-
flex-basis: 31%;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
:host([orientation="horizontal"]) .column {
|
|
408
|
-
padding: var(--tt-space-3);
|
|
409
|
-
}
|
|
485
|
+
:host([size="small"]) .actions > .action,
|
|
486
|
+
:host([orientation="horizontal"]) .actions > .action {
|
|
487
|
+
flex: none;
|
|
410
488
|
}
|
|
411
489
|
`]; }
|
|
412
|
-
renderMedia(inTile) {
|
|
413
|
-
return html `
|
|
414
|
-
<div
|
|
415
|
-
class="media"
|
|
416
|
-
part="media"
|
|
417
|
-
slot=${ifDefined(inTile ? "image" : undefined)}
|
|
418
|
-
>
|
|
419
|
-
<div class="badge"><slot name="badge"></slot></div>
|
|
420
|
-
|
|
421
|
-
<tt-image
|
|
422
|
-
part="image"
|
|
423
|
-
radius="none"
|
|
424
|
-
ratio=${this.ratio}
|
|
425
|
-
src=${this.src}
|
|
426
|
-
alt=${this.heading}
|
|
427
|
-
></tt-image>
|
|
428
|
-
</div>
|
|
429
|
-
`;
|
|
430
|
-
}
|
|
431
|
-
renderRating() {
|
|
432
|
-
return this.rating
|
|
433
|
-
? html `
|
|
434
|
-
<div class="rating" part="rating">
|
|
435
|
-
<tt-icon name="star" size="12px"></tt-icon>
|
|
436
|
-
<span>${this.rating}</span>
|
|
437
|
-
</div>
|
|
438
|
-
`
|
|
439
|
-
: nothing;
|
|
440
|
-
}
|
|
441
|
-
renderBody(withRating = false) {
|
|
442
|
-
const descStyles = styleMap({
|
|
443
|
-
"--_desc-lines": String(this.descriptionLines),
|
|
444
|
-
});
|
|
445
|
-
return html `
|
|
446
|
-
<div class="body" part="body">
|
|
447
|
-
<h2 part="heading">${this.heading}</h2>
|
|
448
|
-
|
|
449
|
-
${this.price
|
|
450
|
-
? html `
|
|
451
|
-
<div class="price" part="price">
|
|
452
|
-
<span>${this.price}</span>${this.wasPrice
|
|
453
|
-
? html `<span class="was"
|
|
454
|
-
><span class="sr-only">Was </span>${this.wasPrice}</span
|
|
455
|
-
>`
|
|
456
|
-
: nothing}
|
|
457
|
-
</div>
|
|
458
|
-
`
|
|
459
|
-
: nothing}
|
|
460
|
-
|
|
461
|
-
${this.description
|
|
462
|
-
? html `
|
|
463
|
-
<p part="description" style=${descStyles}>${this.description}
|
|
464
|
-
</p>
|
|
465
|
-
`
|
|
466
|
-
: nothing}
|
|
467
|
-
|
|
468
|
-
${!withRating
|
|
469
|
-
? html `
|
|
470
|
-
<div class="foot" part="foot">
|
|
471
|
-
${this.renderRating()}${this.renderQuickAdd()}
|
|
472
|
-
</div>
|
|
473
|
-
`
|
|
474
|
-
: nothing}
|
|
475
|
-
</div>
|
|
476
|
-
`;
|
|
477
|
-
}
|
|
478
490
|
/*
|
|
479
491
|
The button's accessible name carries the product: "Add to basket,
|
|
480
492
|
Linen lamp". A grid of twelve products otherwise reads as twelve
|
|
481
|
-
identical "Add to basket" buttons
|
|
482
|
-
|
|
483
|
-
what a sighted user sees (WCAG 2.5.3, label in name).
|
|
493
|
+
identical "Add to basket" buttons. The visible text comes first, so
|
|
494
|
+
the name still starts with what a sighted user sees (WCAG 2.5.3).
|
|
484
495
|
*/
|
|
485
496
|
get actionName() {
|
|
486
497
|
return this.heading ? `${this.buttonLabel}, ${this.heading}` : this.buttonLabel;
|
|
487
498
|
}
|
|
488
|
-
|
|
499
|
+
renderAction() {
|
|
500
|
+
if (!this.buttonLabel)
|
|
501
|
+
return nothing;
|
|
502
|
+
// Small: a round bag icon, so the price keeps its line on a 150px tile.
|
|
503
|
+
if (this.size === "small") {
|
|
504
|
+
return html `
|
|
505
|
+
<tt-button
|
|
506
|
+
class="action"
|
|
507
|
+
part="button"
|
|
508
|
+
variant="secondary"
|
|
509
|
+
pill
|
|
510
|
+
label=${this.actionName}
|
|
511
|
+
?disabled=${this.disabled}
|
|
512
|
+
@tt-button-click=${this.handleAction}
|
|
513
|
+
>
|
|
514
|
+
<tt-icon name="shopping-bag"></tt-icon>
|
|
515
|
+
</tt-button>
|
|
516
|
+
`;
|
|
517
|
+
}
|
|
489
518
|
return html `
|
|
490
519
|
<tt-button
|
|
491
|
-
class="
|
|
520
|
+
class="action"
|
|
492
521
|
part="button"
|
|
493
|
-
|
|
494
|
-
size="small"
|
|
522
|
+
variant="secondary"
|
|
495
523
|
label=${this.actionName}
|
|
496
524
|
?disabled=${this.disabled}
|
|
497
|
-
@tt-button-click=${this.
|
|
525
|
+
@tt-button-click=${this.handleAction}
|
|
498
526
|
>
|
|
499
|
-
|
|
527
|
+
${this.buttonLabel}
|
|
500
528
|
</tt-button>
|
|
501
529
|
`;
|
|
502
530
|
}
|
|
503
|
-
|
|
531
|
+
renderHeading() {
|
|
532
|
+
if (!this.heading)
|
|
533
|
+
return nothing;
|
|
534
|
+
const name = this.url
|
|
535
|
+
? html `<a class="link" part="link" href=${this.url}>${this.heading}</a>`
|
|
536
|
+
: this.clickable
|
|
537
|
+
? html `<button class="link" part="link" type="button" @click=${this.handleOpen}>
|
|
538
|
+
${this.heading}
|
|
539
|
+
</button>`
|
|
540
|
+
: this.heading;
|
|
504
541
|
return html `
|
|
505
|
-
<
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
542
|
+
<div class="heading" part="heading" role="heading" aria-level=${this.headingLevel}>
|
|
543
|
+
${name}
|
|
544
|
+
</div>
|
|
545
|
+
`;
|
|
546
|
+
}
|
|
547
|
+
/*
|
|
548
|
+
Read as "Sale price EUR 89.00, was EUR 109.00". A strike-through is
|
|
549
|
+
not announced by screen readers, so without the hidden words the two
|
|
550
|
+
prices were read as one list of numbers.
|
|
551
|
+
*/
|
|
552
|
+
renderPrice() {
|
|
553
|
+
if (!this.price)
|
|
554
|
+
return nothing;
|
|
555
|
+
const sale = this.wasPrice !== "";
|
|
556
|
+
return html `
|
|
557
|
+
<p class="price" part="price">${sale
|
|
558
|
+
? html `<span class="sr-only">Sale price </span>`
|
|
559
|
+
: nothing}<span class="now ${sale ? "sale" : ""}">${this.price}</span>${sale
|
|
560
|
+
? html `<span class="sr-only">, was </span><s class="was">${this.wasPrice}</s>`
|
|
561
|
+
: nothing}</p>
|
|
514
562
|
`;
|
|
515
563
|
}
|
|
516
564
|
render() {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
565
|
+
const ratio = RATIO.test(this.ratio) ? this.ratio : "1";
|
|
566
|
+
const showMeta = this.rating !== "" || this.hasMeta;
|
|
567
|
+
const showActions = this.buttonLabel !== "" || this.hasActions;
|
|
568
|
+
return html `
|
|
569
|
+
<div class="card ${this.interactive ? "interactive" : ""}" part="card">
|
|
570
|
+
<div class="media" part="media" style=${styleMap({ "--_ratio": ratio })}>
|
|
571
|
+
${this.src !== "" || !this.hasMedia
|
|
572
|
+
? html `<tt-image class="image" part="image" radius="none" src=${this.src} alt=""></tt-image>`
|
|
573
|
+
: nothing}
|
|
574
|
+
<slot name="media" @slotchange=${this.onMedia}></slot>
|
|
575
|
+
<div class="badges"><slot name="badge"></slot></div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div class="body" part="body">
|
|
579
|
+
${this.renderHeading()}
|
|
580
|
+
${this.description
|
|
581
|
+
? html `<p
|
|
582
|
+
class="description"
|
|
583
|
+
part="description"
|
|
584
|
+
style=${styleMap({ "--_lines": String(this.descriptionLines) })}
|
|
585
|
+
>${this.description}</p>`
|
|
586
|
+
: nothing}
|
|
587
|
+
|
|
588
|
+
<div class="meta" part="meta" ?hidden=${!showMeta}>
|
|
589
|
+
${this.rating
|
|
590
|
+
? html `<span class="rating" part="rating">
|
|
591
|
+
<tt-icon name="star"></tt-icon>
|
|
592
|
+
<span class="sr-only">Rating </span>${this.rating}
|
|
593
|
+
</span>`
|
|
594
|
+
: nothing}
|
|
595
|
+
<slot name="meta" @slotchange=${this.onMeta}></slot>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<div class="foot ${this.hasActions ? "extra" : ""}" ?hidden=${!this.price && !showActions}>
|
|
599
|
+
${this.renderPrice()}
|
|
600
|
+
<div class="actions" part="actions" ?hidden=${!showActions}>
|
|
601
|
+
${this.renderAction()}
|
|
602
|
+
<slot name="actions" @slotchange=${this.onActions}></slot>
|
|
528
603
|
</div>
|
|
529
604
|
</div>
|
|
530
|
-
</
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
return html `
|
|
534
|
-
<tt-card part="card" padding="small">
|
|
535
|
-
${this.renderMedia(true)}
|
|
536
|
-
${this.renderBody()}
|
|
537
|
-
</tt-card>
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
538
607
|
`;
|
|
539
608
|
}
|
|
540
609
|
};
|
|
541
|
-
__decorate([
|
|
542
|
-
property({ type: String })
|
|
543
|
-
], TTProduct.prototype, "src", void 0);
|
|
544
|
-
__decorate([
|
|
545
|
-
property({ type: String })
|
|
546
|
-
], TTProduct.prototype, "ratio", void 0);
|
|
547
|
-
__decorate([
|
|
548
|
-
property({ type: String, reflect: true })
|
|
549
|
-
], TTProduct.prototype, "orientation", void 0);
|
|
550
|
-
__decorate([
|
|
551
|
-
property({ type: String, reflect: true })
|
|
552
|
-
], TTProduct.prototype, "variant", void 0);
|
|
553
|
-
__decorate([
|
|
554
|
-
property({ type: String, reflect: true })
|
|
555
|
-
], TTProduct.prototype, "layout", void 0);
|
|
556
610
|
__decorate([
|
|
557
611
|
property({ type: String })
|
|
558
612
|
], TTProduct.prototype, "heading", void 0);
|
|
613
|
+
__decorate([
|
|
614
|
+
property({ type: Number, attribute: "heading-level" })
|
|
615
|
+
], TTProduct.prototype, "headingLevel", void 0);
|
|
559
616
|
__decorate([
|
|
560
617
|
property({ type: String })
|
|
561
618
|
], TTProduct.prototype, "description", void 0);
|
|
@@ -571,18 +628,42 @@ __decorate([
|
|
|
571
628
|
__decorate([
|
|
572
629
|
property({ type: String })
|
|
573
630
|
], TTProduct.prototype, "rating", void 0);
|
|
631
|
+
__decorate([
|
|
632
|
+
property({ type: String })
|
|
633
|
+
], TTProduct.prototype, "src", void 0);
|
|
634
|
+
__decorate([
|
|
635
|
+
property({ type: String })
|
|
636
|
+
], TTProduct.prototype, "ratio", void 0);
|
|
574
637
|
__decorate([
|
|
575
638
|
property({ type: String })
|
|
576
639
|
], TTProduct.prototype, "value", void 0);
|
|
577
640
|
__decorate([
|
|
578
|
-
property({ type: String
|
|
579
|
-
], TTProduct.prototype, "
|
|
641
|
+
property({ type: String })
|
|
642
|
+
], TTProduct.prototype, "href", void 0);
|
|
643
|
+
__decorate([
|
|
644
|
+
property({ type: Boolean, reflect: true })
|
|
645
|
+
], TTProduct.prototype, "clickable", void 0);
|
|
580
646
|
__decorate([
|
|
581
647
|
property({ type: String, attribute: "button-label" })
|
|
582
648
|
], TTProduct.prototype, "buttonLabel", void 0);
|
|
649
|
+
__decorate([
|
|
650
|
+
property({ type: String, reflect: true })
|
|
651
|
+
], TTProduct.prototype, "orientation", void 0);
|
|
652
|
+
__decorate([
|
|
653
|
+
property({ type: String, reflect: true })
|
|
654
|
+
], TTProduct.prototype, "size", void 0);
|
|
583
655
|
__decorate([
|
|
584
656
|
property({ type: Boolean, reflect: true })
|
|
585
657
|
], TTProduct.prototype, "disabled", void 0);
|
|
658
|
+
__decorate([
|
|
659
|
+
state()
|
|
660
|
+
], TTProduct.prototype, "hasMedia", void 0);
|
|
661
|
+
__decorate([
|
|
662
|
+
state()
|
|
663
|
+
], TTProduct.prototype, "hasMeta", void 0);
|
|
664
|
+
__decorate([
|
|
665
|
+
state()
|
|
666
|
+
], TTProduct.prototype, "hasActions", void 0);
|
|
586
667
|
TTProduct = __decorate([
|
|
587
668
|
customElement("tt-product")
|
|
588
669
|
], TTProduct);
|