@unofficialbox/box-open-elements 0.11.0 → 0.13.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.
Files changed (59) hide show
  1. package/dist/components/feedback/alert.d.ts +2 -0
  2. package/dist/components/feedback/alert.js +41 -8
  3. package/dist/components/feedback/indicator.d.ts +34 -0
  4. package/dist/components/feedback/indicator.js +167 -0
  5. package/dist/components/feedback/nudge.js +0 -1
  6. package/dist/components/feedback/toast.d.ts +4 -5
  7. package/dist/components/feedback/toast.js +19 -46
  8. package/dist/components/feedback/tone.d.ts +18 -0
  9. package/dist/components/feedback/tone.js +23 -0
  10. package/dist/components/forms/calendar.d.ts +25 -0
  11. package/dist/components/forms/calendar.js +94 -2
  12. package/dist/components/forms/tile-group.d.ts +58 -0
  13. package/dist/components/forms/tile-group.js +280 -0
  14. package/dist/components/output/code-block.d.ts +49 -0
  15. package/dist/components/output/code-block.js +236 -0
  16. package/dist/components/output/formatted-date.d.ts +38 -0
  17. package/dist/components/output/formatted-date.js +90 -0
  18. package/dist/components/output/formatted-file-size.d.ts +31 -0
  19. package/dist/components/output/formatted-file-size.js +61 -0
  20. package/dist/components/output/formatted-number.d.ts +37 -0
  21. package/dist/components/output/formatted-number.js +106 -0
  22. package/dist/components/output/formatted-value.d.ts +44 -0
  23. package/dist/components/output/formatted-value.js +100 -0
  24. package/dist/components/output/index.d.ts +6 -0
  25. package/dist/components/output/index.js +9 -0
  26. package/dist/components/output/relative-time.d.ts +41 -0
  27. package/dist/components/output/relative-time.js +82 -0
  28. package/dist/element-maps.d.ts +17 -1
  29. package/dist/entries/code-block.d.ts +1 -0
  30. package/dist/entries/code-block.js +1 -0
  31. package/dist/entries/comment-thread.d.ts +1 -0
  32. package/dist/entries/comment-thread.js +1 -0
  33. package/dist/entries/formatted-date.d.ts +1 -0
  34. package/dist/entries/formatted-date.js +1 -0
  35. package/dist/entries/formatted-file-size.d.ts +1 -0
  36. package/dist/entries/formatted-file-size.js +1 -0
  37. package/dist/entries/formatted-number.d.ts +1 -0
  38. package/dist/entries/formatted-number.js +1 -0
  39. package/dist/entries/indicator.d.ts +1 -0
  40. package/dist/entries/indicator.js +1 -0
  41. package/dist/entries/relative-time.d.ts +1 -0
  42. package/dist/entries/relative-time.js +1 -0
  43. package/dist/entries/tile-group.d.ts +1 -0
  44. package/dist/entries/tile-group.js +1 -0
  45. package/dist/foundations/format/format.d.ts +82 -0
  46. package/dist/foundations/format/format.js +122 -0
  47. package/dist/foundations/format/index.d.ts +1 -0
  48. package/dist/foundations/format/index.js +3 -0
  49. package/dist/index.d.ts +10 -0
  50. package/dist/index.js +12 -0
  51. package/dist/patterns/comments/comment-thread.d.ts +79 -0
  52. package/dist/patterns/comments/comment-thread.js +561 -0
  53. package/dist/patterns/comments/index.d.ts +2 -0
  54. package/dist/patterns/comments/index.js +5 -0
  55. package/dist/patterns/comments/types.d.ts +39 -0
  56. package/dist/patterns/comments/types.js +8 -0
  57. package/dist/patterns/preview/annotation-thread.d.ts +67 -48
  58. package/dist/patterns/preview/annotation-thread.js +96 -440
  59. package/package.json +1 -1
@@ -172,6 +172,15 @@ const calendarStyles = `
172
172
  background: var(--boe-token-surface-surface-brand-hover, #006ae9);
173
173
  }
174
174
 
175
+ /* The span between the endpoints. A tint rather than the brand fill, so the
176
+ two chosen dates stay the things the eye lands on — the middle is context,
177
+ not a selection the reader made. */
178
+ [part~="day"][data-in-range="true"]:not([data-selected="true"]) {
179
+ background: var(--boe-token-surface-item-surface-selected, #f2f7fd);
180
+ border-color: transparent;
181
+ color: var(--boe-token-text-text, #222222);
182
+ }
183
+
175
184
  [part~="day"]:focus-visible {
176
185
  outline: none;
177
186
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 24%, transparent);
@@ -185,7 +194,7 @@ const calendarStyles = `
185
194
  export class Calendar extends BaseElement {
186
195
  static tagName = DEFAULT_TAG_NAME;
187
196
  static get observedAttributes() {
188
- return ["disabled", "max", "min", "month", "today", "value"];
197
+ return ["disabled", "end", "max", "min", "mode", "month", "start", "today", "value"];
189
198
  }
190
199
  /** The date the roving tabindex currently points at. */
191
200
  activeDate = null;
@@ -283,11 +292,83 @@ export class Calendar extends BaseElement {
283
292
  const next = new Date(Date.UTC(y, m - 1 + delta, 1));
284
293
  this.goToMonth(next.getUTCFullYear(), next.getUTCMonth() + 1);
285
294
  }
295
+ /**
296
+ * `single` (default) or `range`.
297
+ *
298
+ * Range mode uses `start`/`end` rather than overloading `value`: a single
299
+ * attribute holding two dates would have to invent a separator, and every
300
+ * host would then have to parse it back out.
301
+ */
302
+ get mode() {
303
+ return this.getAttribute("mode") === "range" ? "range" : "single";
304
+ }
305
+ set mode(next) {
306
+ this.setAttribute("mode", next);
307
+ }
308
+ /** First date of the range, ISO. */
309
+ get start() {
310
+ return this.getAttribute("start") ?? "";
311
+ }
312
+ set start(next) {
313
+ if (!next) {
314
+ this.removeAttribute("start");
315
+ return;
316
+ }
317
+ this.setAttribute("start", next);
318
+ }
319
+ /** Last date of the range, ISO. Empty while only one end has been picked. */
320
+ get end() {
321
+ return this.getAttribute("end") ?? "";
322
+ }
323
+ set end(next) {
324
+ if (!next) {
325
+ this.removeAttribute("end");
326
+ return;
327
+ }
328
+ this.setAttribute("end", next);
329
+ }
330
+ /**
331
+ * Advance the range by one click.
332
+ *
333
+ * Three states, cycling: nothing chosen picks a start; a start alone closes
334
+ * the range; a complete range starts over. Picking an earlier day as the
335
+ * second click swaps the ends rather than rejecting it — a reader dragging
336
+ * backwards through a calendar means the same thing as dragging forwards,
337
+ * and refusing it would be pedantry.
338
+ */
339
+ selectRange(iso) {
340
+ const start = this.start;
341
+ const end = this.end;
342
+ if (!start || end) {
343
+ this.start = iso;
344
+ this.end = "";
345
+ }
346
+ else if (iso < start) {
347
+ this.end = start;
348
+ this.start = iso;
349
+ }
350
+ else {
351
+ this.end = iso;
352
+ }
353
+ this.dispatchEvent(new CustomEvent("range-changed", {
354
+ bubbles: true,
355
+ composed: true,
356
+ detail: { end: this.end, start: this.start },
357
+ }));
358
+ }
286
359
  selectDate(date) {
287
360
  const iso = toISO(date);
288
361
  if (this.disabled || this.isOutOfRange(iso)) {
289
362
  return;
290
363
  }
364
+ if (this.mode === "range") {
365
+ this.selectRange(iso);
366
+ this.setAttribute("month", `${date.y}-${pad(date.m)}`);
367
+ this.activeDate = date;
368
+ this.update();
369
+ this.focusActive();
370
+ return;
371
+ }
291
372
  this.setAttribute("value", iso);
292
373
  // Keep the grid on the month of the chosen day.
293
374
  this.setAttribute("month", `${date.y}-${pad(date.m)}`);
@@ -446,6 +527,9 @@ export class Calendar extends BaseElement {
446
527
  }
447
528
  const { y, m } = this.displayedMonth();
448
529
  const selected = parseISO(this.value);
530
+ const isRange = this.mode === "range";
531
+ const rangeStart = isRange ? this.start : "";
532
+ const rangeEnd = isRange ? this.end : "";
449
533
  const active = this.resolveActiveDate();
450
534
  const activeIso = toISO({ y, m, d: clampDay(y, m, active.d) });
451
535
  const todayIso = toISO(this.resolveToday());
@@ -459,7 +543,14 @@ export class Calendar extends BaseElement {
459
543
  }
460
544
  for (let day = 1; day <= total; day += 1) {
461
545
  const iso = toISO({ y, m, d: day });
462
- const isSelected = selected ? toISO(selected) === iso : false;
546
+ const isSelected = isRange
547
+ ? iso === rangeStart || iso === rangeEnd
548
+ : selected
549
+ ? toISO(selected) === iso
550
+ : false;
551
+ // Strictly between the ends: the endpoints are "selected", not "in range",
552
+ // so they keep the brand fill rather than the tint.
553
+ const inRange = isRange && rangeStart !== "" && rangeEnd !== "" && iso > rangeStart && iso < rangeEnd;
463
554
  const isToday = iso === todayIso;
464
555
  const isActive = iso === activeIso;
465
556
  const outOfRange = this.isOutOfRange(iso);
@@ -470,6 +561,7 @@ export class Calendar extends BaseElement {
470
561
  role="gridcell"
471
562
  data-date="${iso}"
472
563
  data-selected="${isSelected ? "true" : "false"}"
564
+ data-in-range="${inRange ? "true" : "false"}"
473
565
  data-today="${isToday ? "true" : "false"}"
474
566
  tabindex="${isActive && !disabled ? "0" : "-1"}"
475
567
  aria-selected="${isSelected ? "true" : "false"}"
@@ -0,0 +1,58 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ /** One choice. `description` is the second line; `disabled` greys it out. */
3
+ export interface TileOption {
4
+ description?: string;
5
+ disabled?: boolean;
6
+ id: string;
7
+ label: string;
8
+ }
9
+ /** Attribute payloads are author input — validate every record. */
10
+ export declare const isTileOptionRecord: (value: unknown) => value is TileOption;
11
+ /**
12
+ * Choose one option, or several, as cards rather than radios.
13
+ *
14
+ * ```html
15
+ * <box-tile-group name="plan" options='[{"id":"team","label":"Team"}]'></box-tile-group>
16
+ * ```
17
+ *
18
+ * Each tile wraps a real `<input type="radio">` or `<input type="checkbox">`,
19
+ * visually hidden but present in the tab order and the accessibility tree. That
20
+ * is the whole design: grouping, arrow-key navigation between radios, the
21
+ * roving focus rules, and form participation all come from the platform rather
22
+ * than being reimplemented on `<div>`s and then subtly diverging from what a
23
+ * screen reader user expects.
24
+ *
25
+ * Use it where the choice deserves explaining — a plan, a permission level, a
26
+ * retention policy — and a bare radio label would not have room. Where the
27
+ * options are one word each, radios are smaller and better.
28
+ */
29
+ export declare class TileGroup extends BaseElement {
30
+ static readonly tagName: string;
31
+ static get observedAttributes(): string[];
32
+ private groupEl;
33
+ private legendEl;
34
+ private optionsSignature;
35
+ /** The choices, as JSON. */
36
+ get options(): TileOption[];
37
+ set options(value: TileOption[]);
38
+ /** Several selections rather than one. Switches radios for checkboxes. */
39
+ get multiple(): boolean;
40
+ set multiple(value: boolean);
41
+ /** The radio group name. Required for radios to behave as one group. */
42
+ get name(): string;
43
+ set name(value: string);
44
+ /** Selected ids, comma-separated. A single id when not `multiple`. */
45
+ get value(): string;
46
+ set value(next: string);
47
+ /** Selected ids as an array, which is what a host actually wants. */
48
+ get selected(): string[];
49
+ get legend(): string;
50
+ set legend(value: string);
51
+ private optionsKey;
52
+ private rebuild;
53
+ private syncSelection;
54
+ private readSelection;
55
+ protected renderTemplate(): void;
56
+ protected setupListeners(): void;
57
+ protected update(): void;
58
+ }
@@ -0,0 +1,280 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ import { boeRadius } from "../../foundations/geometry/index.js";
3
+ import { boeFocusRingShadow } from "../../foundations/tokens/index.js";
4
+ const DEFAULT_TAG_NAME = "box-tile-group";
5
+ const escapeHtml = (value) => value
6
+ .replaceAll("&", "&amp;")
7
+ .replaceAll("<", "&lt;")
8
+ .replaceAll(">", "&gt;")
9
+ .replaceAll('"', "&quot;")
10
+ .replaceAll("'", "&#39;");
11
+ /** Attribute payloads are author input — validate every record. */
12
+ export const isTileOptionRecord = (value) => {
13
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
14
+ return false;
15
+ }
16
+ const item = value;
17
+ return typeof item.id === "string" && typeof item.label === "string";
18
+ };
19
+ const tileStyles = `
20
+ :host {
21
+ display: block;
22
+ color: inherit;
23
+ font: inherit;
24
+ }
25
+
26
+ :host([hidden]) {
27
+ display: none;
28
+ }
29
+
30
+ [part="group"] {
31
+ display: grid;
32
+ /* Tiles carry a label and often a description, so they need width to be
33
+ worth choosing over radios. Below the minimum they stack rather than
34
+ squeeze. */
35
+ grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
36
+ gap: 0.6rem;
37
+ margin: 0;
38
+ padding: 0;
39
+ border: 0;
40
+ }
41
+
42
+ [part="legend"] {
43
+ padding: 0 0 0.5rem;
44
+ font-weight: 700;
45
+ color: var(--boe-token-text-text, #222222);
46
+ }
47
+
48
+ [part="legend"]:empty {
49
+ display: none;
50
+ }
51
+
52
+ [part="tile"] {
53
+ position: relative;
54
+ display: grid;
55
+ gap: 0.2rem;
56
+ padding: 0.75rem 0.85rem;
57
+ border: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 85%, transparent);
58
+ border-radius: ${boeRadius.large};
59
+ background: var(--boe-token-surface-surface, #ffffff);
60
+ cursor: pointer;
61
+ }
62
+
63
+ [part="tile"]:hover:not([data-disabled="true"]) {
64
+ background: var(--boe-token-surface-surface-hover, #f4f4f4);
65
+ }
66
+
67
+ [part="tile"][data-selected="true"] {
68
+ border-color: var(--boe-token-surface-surface-brand, #0061d5);
69
+ background: var(--boe-token-surface-item-surface-selected, #f2f7fd);
70
+ }
71
+
72
+ [part="tile"][data-disabled="true"] {
73
+ cursor: not-allowed;
74
+ opacity: 0.5;
75
+ }
76
+
77
+ /* The ring is drawn on the tile the native control sits inside, because the
78
+ control itself is visually hidden — without this, keyboard focus would be
79
+ invisible. */
80
+ [part="tile"]:has(input:focus-visible) {
81
+ box-shadow: ${boeFocusRingShadow};
82
+ }
83
+
84
+ /* Hidden from sight, not from the accessibility tree or the tab order: the
85
+ tile is a real radio or checkbox, so grouping, arrow-key behaviour and
86
+ form participation are the platform's rather than reimplemented. */
87
+ [part="control"] {
88
+ position: absolute;
89
+ inline-size: 1px;
90
+ block-size: 1px;
91
+ padding: 0;
92
+ margin: -1px;
93
+ overflow: hidden;
94
+ clip-path: inset(50%);
95
+ white-space: nowrap;
96
+ }
97
+
98
+ [part="tile-label"] {
99
+ font-weight: 600;
100
+ color: var(--boe-token-text-text, #222222);
101
+ }
102
+
103
+ [part="tile-description"] {
104
+ font-size: 0.82rem;
105
+ line-height: 1.45;
106
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
107
+ }
108
+
109
+ [part="tile-description"]:empty {
110
+ display: none;
111
+ }
112
+ `;
113
+ /**
114
+ * Choose one option, or several, as cards rather than radios.
115
+ *
116
+ * ```html
117
+ * <box-tile-group name="plan" options='[{"id":"team","label":"Team"}]'></box-tile-group>
118
+ * ```
119
+ *
120
+ * Each tile wraps a real `<input type="radio">` or `<input type="checkbox">`,
121
+ * visually hidden but present in the tab order and the accessibility tree. That
122
+ * is the whole design: grouping, arrow-key navigation between radios, the
123
+ * roving focus rules, and form participation all come from the platform rather
124
+ * than being reimplemented on `<div>`s and then subtly diverging from what a
125
+ * screen reader user expects.
126
+ *
127
+ * Use it where the choice deserves explaining — a plan, a permission level, a
128
+ * retention policy — and a bare radio label would not have room. Where the
129
+ * options are one word each, radios are smaller and better.
130
+ */
131
+ export class TileGroup extends BaseElement {
132
+ static tagName = DEFAULT_TAG_NAME;
133
+ static get observedAttributes() {
134
+ return ["legend", "multiple", "name", "options", "value"];
135
+ }
136
+ groupEl;
137
+ legendEl;
138
+ optionsSignature = "";
139
+ /** The choices, as JSON. */
140
+ get options() {
141
+ const raw = this.getAttribute("options");
142
+ if (!raw) {
143
+ return [];
144
+ }
145
+ try {
146
+ const parsed = JSON.parse(raw);
147
+ return Array.isArray(parsed) && parsed.every(isTileOptionRecord)
148
+ ? parsed
149
+ : [];
150
+ }
151
+ catch {
152
+ return [];
153
+ }
154
+ }
155
+ set options(value) {
156
+ this.setAttribute("options", JSON.stringify(value));
157
+ }
158
+ /** Several selections rather than one. Switches radios for checkboxes. */
159
+ get multiple() {
160
+ return this.hasAttribute("multiple");
161
+ }
162
+ set multiple(value) {
163
+ this.toggleAttribute("multiple", value);
164
+ }
165
+ /** The radio group name. Required for radios to behave as one group. */
166
+ get name() {
167
+ return this.getAttribute("name") ?? "tile-group";
168
+ }
169
+ set name(value) {
170
+ this.setAttribute("name", value);
171
+ }
172
+ /** Selected ids, comma-separated. A single id when not `multiple`. */
173
+ get value() {
174
+ return this.getAttribute("value") ?? "";
175
+ }
176
+ set value(next) {
177
+ if (!next) {
178
+ this.removeAttribute("value");
179
+ return;
180
+ }
181
+ this.setAttribute("value", next);
182
+ }
183
+ /** Selected ids as an array, which is what a host actually wants. */
184
+ get selected() {
185
+ return this.value
186
+ .split(",")
187
+ .map(part => part.trim())
188
+ .filter(Boolean);
189
+ }
190
+ get legend() {
191
+ return this.getAttribute("legend") ?? "";
192
+ }
193
+ set legend(value) {
194
+ this.setAttribute("legend", value);
195
+ }
196
+ optionsKey() {
197
+ return `${this.getAttribute("options") ?? ""}|${String(this.multiple)}|${this.name}`;
198
+ }
199
+ rebuild() {
200
+ const type = this.multiple ? "checkbox" : "radio";
201
+ this.groupEl.innerHTML = this.options
202
+ .map(option => {
203
+ const id = escapeHtml(option.id);
204
+ return `
205
+ <label part="tile" data-option-id="${id}" data-selected="false" data-disabled="${option.disabled ? "true" : "false"}">
206
+ <input
207
+ part="control"
208
+ type="${type}"
209
+ name="${escapeHtml(this.name)}"
210
+ value="${id}"
211
+ ${option.disabled ? "disabled" : ""}
212
+ />
213
+ <span part="tile-label">${escapeHtml(option.label)}</span>
214
+ <span part="tile-description">${escapeHtml(option.description ?? "")}</span>
215
+ </label>
216
+ `;
217
+ })
218
+ .join("");
219
+ }
220
+ syncSelection() {
221
+ const selected = new Set(this.selected);
222
+ this.groupEl.querySelectorAll('[part="tile"]').forEach(tile => {
223
+ const id = tile.dataset.optionId ?? "";
224
+ const isSelected = selected.has(id);
225
+ tile.dataset.selected = isSelected ? "true" : "false";
226
+ const control = tile.querySelector('[part="control"]');
227
+ if (control) {
228
+ control.checked = isSelected;
229
+ }
230
+ });
231
+ }
232
+ readSelection() {
233
+ return Array.from(this.groupEl.querySelectorAll('[part="control"]'))
234
+ .filter(input => input.checked)
235
+ .map(input => input.value);
236
+ }
237
+ renderTemplate() {
238
+ if (!this.shadowRoot) {
239
+ return;
240
+ }
241
+ this.shadowRoot.innerHTML = `
242
+ <style>${tileStyles}</style>
243
+ <fieldset part="group">
244
+ <legend part="legend"></legend>
245
+ </fieldset>
246
+ `;
247
+ this.groupEl = this.shadowRoot.querySelector('[part="group"]');
248
+ this.legendEl = this.shadowRoot.querySelector('[part="legend"]');
249
+ }
250
+ setupListeners() {
251
+ this.groupEl.addEventListener("change", () => {
252
+ const next = this.readSelection();
253
+ this.value = next.join(",");
254
+ this.dispatchEvent(new CustomEvent("tile-change", {
255
+ bubbles: true,
256
+ composed: true,
257
+ detail: { selected: next },
258
+ }));
259
+ });
260
+ }
261
+ update() {
262
+ if (!this.groupEl) {
263
+ return;
264
+ }
265
+ const nextKey = this.optionsKey();
266
+ if (nextKey !== this.optionsSignature) {
267
+ this.optionsSignature = nextKey;
268
+ // The legend lives inside the fieldset and is wiped by the rebuild, so it
269
+ // is re-created rather than queried once at render time.
270
+ this.rebuild();
271
+ const legend = document.createElement("legend");
272
+ legend.setAttribute("part", "legend");
273
+ this.groupEl.prepend(legend);
274
+ this.legendEl = legend;
275
+ }
276
+ this.legendEl.textContent = this.legend;
277
+ this.syncSelection();
278
+ }
279
+ }
280
+ TileGroup.register();
@@ -0,0 +1,49 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ /**
3
+ * A block of code, rendered as written.
4
+ *
5
+ * ```html
6
+ * <box-code-block code="npm i @unofficialbox/box-open-elements"></box-code-block>
7
+ * ```
8
+ *
9
+ * The code is set as `textContent`, never as markup — a snippet is the most
10
+ * likely string on any page to contain angle brackets, and rendering it as HTML
11
+ * would both corrupt what the reader sees and hand an injection point to
12
+ * whatever produced the snippet.
13
+ *
14
+ * There is no syntax highlighting, deliberately. Doing it properly means
15
+ * shipping a grammar per language, and doing it improperly means mis-colouring
16
+ * code that a reader is trying to trust. A host that needs highlighting can
17
+ * slot pre-rendered markup into the default slot instead.
18
+ *
19
+ * Long lines scroll rather than wrap: a wrapped line silently changes what the
20
+ * reader believes the source says. `wrap` opts into wrapping where the content
21
+ * has no meaningful column structure.
22
+ */
23
+ export declare class CodeBlock extends BaseElement {
24
+ static readonly tagName: string;
25
+ static get observedAttributes(): string[];
26
+ private preEl;
27
+ private codeEl;
28
+ private copyEl;
29
+ private slotEl;
30
+ private resetTimer;
31
+ /** The source to render. */
32
+ get code(): string;
33
+ set code(value: string);
34
+ /**
35
+ * The language, surfaced as a `data-language` hook and in the code element's
36
+ * class the way `highlight.js` and friends expect. Nothing here reads it.
37
+ */
38
+ get language(): string;
39
+ set language(value: string);
40
+ /** Accessible name for the copy button. */
41
+ get copyLabel(): string;
42
+ set copyLabel(value: string);
43
+ private copy;
44
+ private markCopied;
45
+ disconnectedCallback(): void;
46
+ protected renderTemplate(): void;
47
+ protected setupListeners(): void;
48
+ protected update(): void;
49
+ }