@threadlabs/looma 0.10.11 → 0.11.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 (128) hide show
  1. package/components/shared/editor.js +2 -26
  2. package/components/shared/icons.js +53 -0
  3. package/components/ui-avatar-group/ui-avatar-group.html +6 -6
  4. package/components/ui-avatar-group/ui-avatar-group.js +1 -1
  5. package/components/ui-breadcrumb-item/ui-breadcrumb-item.html +60 -0
  6. package/components/ui-breadcrumbs/ui-breadcrumbs.html +31 -0
  7. package/components/ui-card/ui-card.html +55 -0
  8. package/components/ui-combobox/ui-combobox.js +2 -2
  9. package/components/ui-context-menu/ui-context-menu.js +4 -4
  10. package/components/ui-cover/ui-cover.html +36 -0
  11. package/components/ui-description-item/ui-description-item.html +46 -0
  12. package/components/ui-description-list/ui-description-list.html +37 -0
  13. package/components/ui-icon/ui-icon.js +1 -1
  14. package/components/ui-list/ui-list.html +33 -0
  15. package/components/ui-list-item/ui-list-item.html +146 -0
  16. package/components/ui-menu/ui-menu.js +3 -3
  17. package/components/ui-page-header/ui-page-header.html +97 -0
  18. package/components/ui-radio/ui-radio.js +1 -1
  19. package/components/ui-radio-group/ui-radio-group.js +1 -1
  20. package/components/ui-section/ui-section.html +108 -0
  21. package/components/ui-spinner/ui-spinner.html +48 -0
  22. package/components/ui-status-message/ui-status-message.html +84 -0
  23. package/components/ui-text/ui-text.html +96 -0
  24. package/components/ui-tree/ui-tree.js +2 -2
  25. package/components/ui-tree-item/ui-tree-item.js +3 -3
  26. package/dist/index.js +1 -1
  27. package/editor/icons.d.ts +5 -0
  28. package/editor/index.js +6 -1
  29. package/package.json +1 -1
  30. package/styles/ui-avatar-group.css +6 -6
  31. package/styles/ui-breadcrumb-item.css +57 -0
  32. package/styles/ui-breadcrumbs.css +32 -0
  33. package/styles/ui-card.css +51 -0
  34. package/styles/ui-cover.css +35 -0
  35. package/styles/ui-description-item.css +43 -0
  36. package/styles/ui-description-list.css +38 -0
  37. package/styles/ui-list-item.css +146 -0
  38. package/styles/ui-list.css +33 -0
  39. package/styles/ui-page-header.css +89 -0
  40. package/styles/ui-section.css +94 -0
  41. package/styles/ui-spinner.css +42 -0
  42. package/styles/ui-status-message.css +72 -0
  43. package/styles/ui-text.css +85 -0
  44. package/vanilla/UiBreadcrumbItem.d.ts +14 -0
  45. package/vanilla/UiBreadcrumbItem.js +38 -0
  46. package/vanilla/UiBreadcrumbs.d.ts +14 -0
  47. package/vanilla/UiBreadcrumbs.js +39 -0
  48. package/vanilla/UiCard.d.ts +17 -0
  49. package/vanilla/UiCard.js +32 -0
  50. package/vanilla/UiCover.d.ts +14 -0
  51. package/vanilla/UiCover.js +28 -0
  52. package/vanilla/UiDescriptionItem.d.ts +14 -0
  53. package/vanilla/UiDescriptionItem.js +39 -0
  54. package/vanilla/UiDescriptionList.d.ts +14 -0
  55. package/vanilla/UiDescriptionList.js +28 -0
  56. package/vanilla/UiList.d.ts +14 -0
  57. package/vanilla/UiList.js +29 -0
  58. package/vanilla/UiListItem.d.ts +15 -0
  59. package/vanilla/UiListItem.js +77 -0
  60. package/vanilla/UiPageHeader.d.ts +14 -0
  61. package/vanilla/UiPageHeader.js +78 -0
  62. package/vanilla/UiSection.d.ts +16 -0
  63. package/vanilla/UiSection.js +64 -0
  64. package/vanilla/UiSpinner.d.ts +15 -0
  65. package/vanilla/UiSpinner.js +23 -0
  66. package/vanilla/UiStatusMessage.d.ts +16 -0
  67. package/vanilla/UiStatusMessage.js +53 -0
  68. package/vanilla/UiText.d.ts +20 -0
  69. package/vanilla/UiText.js +38 -0
  70. package/vanilla/index.js +13 -0
  71. package/vue/UiAvatarGroup.vue +6 -6
  72. package/vue/UiBreadcrumbItem.d.ts +18 -0
  73. package/vue/UiBreadcrumbItem.js +2 -0
  74. package/vue/UiBreadcrumbItem.vue +101 -0
  75. package/vue/UiBreadcrumbs.d.ts +18 -0
  76. package/vue/UiBreadcrumbs.js +2 -0
  77. package/vue/UiBreadcrumbs.vue +45 -0
  78. package/vue/UiCard.d.ts +24 -0
  79. package/vue/UiCard.js +2 -0
  80. package/vue/UiCard.vue +82 -0
  81. package/vue/UiCover.d.ts +18 -0
  82. package/vue/UiCover.js +2 -0
  83. package/vue/UiCover.vue +57 -0
  84. package/vue/UiDescriptionItem.d.ts +18 -0
  85. package/vue/UiDescriptionItem.js +2 -0
  86. package/vue/UiDescriptionItem.vue +58 -0
  87. package/vue/UiDescriptionList.d.ts +18 -0
  88. package/vue/UiDescriptionList.js +2 -0
  89. package/vue/UiDescriptionList.vue +62 -0
  90. package/vue/UiIconButton.d.ts +1 -1
  91. package/vue/UiList.d.ts +18 -0
  92. package/vue/UiList.js +2 -0
  93. package/vue/UiList.vue +60 -0
  94. package/vue/UiListItem.d.ts +26 -0
  95. package/vue/UiListItem.js +2 -0
  96. package/vue/UiListItem.vue +171 -0
  97. package/vue/UiPageHeader.d.ts +26 -0
  98. package/vue/UiPageHeader.js +2 -0
  99. package/vue/UiPageHeader.vue +118 -0
  100. package/vue/UiSection.d.ts +26 -0
  101. package/vue/UiSection.js +2 -0
  102. package/vue/UiSection.vue +128 -0
  103. package/vue/UiSpinner.d.ts +10 -0
  104. package/vue/UiSpinner.js +2 -0
  105. package/vue/UiSpinner.vue +87 -0
  106. package/vue/UiStatusMessage.d.ts +24 -0
  107. package/vue/UiStatusMessage.js +2 -0
  108. package/vue/UiStatusMessage.vue +121 -0
  109. package/vue/UiText.d.ts +30 -0
  110. package/vue/UiText.js +2 -0
  111. package/vue/UiText.vue +125 -0
  112. package/vue/chunks/UiAvatarGroup.js +1 -1
  113. package/vue/chunks/UiBreadcrumbItem.js +50 -0
  114. package/vue/chunks/UiBreadcrumbs.js +19 -0
  115. package/vue/chunks/UiCard.js +31 -0
  116. package/vue/chunks/UiCover.js +21 -0
  117. package/vue/chunks/UiDescriptionItem.js +16 -0
  118. package/vue/chunks/UiDescriptionList.js +21 -0
  119. package/vue/chunks/UiList.js +24 -0
  120. package/vue/chunks/UiListItem.js +39 -0
  121. package/vue/chunks/UiPageHeader.js +35 -0
  122. package/vue/chunks/UiSection.js +34 -0
  123. package/vue/chunks/UiSpinner.js +64 -0
  124. package/vue/chunks/UiStatusMessage.js +54 -0
  125. package/vue/chunks/UiText.js +40 -0
  126. package/vue/components.css +661 -29
  127. package/vue/index.d.ts +13 -0
  128. package/vue/index.js +15 -2
@@ -0,0 +1,146 @@
1
+ <template component="ui-list-item">
2
+ <defs>
3
+ <prop name="variant" type="row | card" default="row"
4
+ >How the item is drawn. A row is flat and takes a hover surface when it is a link, for items read down a list. A card sits on a raised, bordered surface,
5
+ for items set in a grid (a List with layout="grid"). Both have the same parts: leading, title, description, and trailing.</prop>
6
+ <prop name="current" type="boolean" default="false"
7
+ >Marks the item as the current one, such as the site you are in: it takes the selected surface and aria-current="true", which assistive technology
8
+ announces. Set it on one item at a time.</prop>
9
+ </defs>
10
+ <li :aria-current="current">
11
+ <span class="leading"><slot name="leading"></slot></span>
12
+ <span class="content">
13
+ <span class="title"><slot></slot></span>
14
+ <span class="description"><slot name="description"></slot></span>
15
+ </span>
16
+ <span class="trailing"><slot name="trailing"></slot></span>
17
+ </li>
18
+ <style>
19
+ /* One entry in a list: something to recognise (leading, usually an icon), its name (the default
20
+ slot), one line about it (description), and what it offers (trailing, such as a button or a
21
+ badge). The title and description are one line each and end in an ellipsis, so every item in a
22
+ list has the same height.
23
+
24
+ A link in the title is the whole item: its hit area stretches over the item, so the icon and the
25
+ padding follow it too, and the item takes the link's focus ring. The link stays a real link, so
26
+ a router, a modified click, and "open in new tab" all work. Trailing controls sit above it and
27
+ stay their own. */
28
+ :host {
29
+ position: relative;
30
+ box-sizing: border-box;
31
+ display: flex;
32
+ align-items: center;
33
+ gap: var(--ui-list-item-gap, var(--ui-space-3));
34
+ min-inline-size: 0;
35
+ min-block-size: var(--ui-control-size, 2.5rem);
36
+ padding-block: var(--ui-list-item-padding-block, var(--ui-space-2));
37
+ padding-inline: var(--ui-list-item-padding-inline, var(--ui-space-3));
38
+ border-radius: var(--ui-radius-md);
39
+ color: var(--ui-text-primary);
40
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease), border-color var(--ui-motion-fast) var(--ui-motion-ease);
41
+ }
42
+
43
+ :host-state([variant="card"]) {
44
+ padding-block: var(--ui-list-item-padding-block, var(--ui-space-3));
45
+ padding-inline: var(--ui-list-item-padding-inline, var(--ui-space-4));
46
+ border: 1px solid var(--ui-border-default);
47
+ border-radius: var(--ui-radius-lg);
48
+ background: var(--ui-surface-raised);
49
+ }
50
+
51
+ :host:has(.title a[href]:hover) {
52
+ background: var(--ui-surface-hover);
53
+ }
54
+
55
+ :host-state([variant="card"]):has(.title a[href]:hover) {
56
+ border-color: var(--ui-border-strong);
57
+ }
58
+
59
+ :host-state([current]) {
60
+ background: var(--ui-surface-hover);
61
+ }
62
+
63
+ :host:has(.title a[href]:focus-visible) {
64
+ box-shadow: 0 0 0 2px var(--ui-focus-ring);
65
+ }
66
+
67
+ .leading,
68
+ .trailing {
69
+ display: inline-flex;
70
+ flex: none;
71
+ align-items: center;
72
+ gap: var(--ui-space-2);
73
+ }
74
+
75
+ .leading {
76
+ color: var(--ui-text-muted);
77
+ font-size: var(--ui-list-item-icon-size, var(--ui-font-size-lg));
78
+ }
79
+
80
+ /* Trailing controls take their own clicks, above the title link's stretched hit area. */
81
+ .trailing {
82
+ position: relative;
83
+ z-index: 1;
84
+ color: var(--ui-text-muted);
85
+ font-size: var(--ui-font-size-sm);
86
+ }
87
+
88
+ .content {
89
+ display: flex;
90
+ flex: 1;
91
+ flex-direction: column;
92
+ gap: var(--ui-space-0-5, 2px);
93
+ min-inline-size: 0;
94
+ }
95
+
96
+ .title,
97
+ .description {
98
+ display: block;
99
+ min-inline-size: 0;
100
+ overflow: hidden;
101
+ text-overflow: ellipsis;
102
+ white-space: nowrap;
103
+ }
104
+
105
+ .title {
106
+ font-size: var(--ui-font-size-md);
107
+ font-weight: var(--ui-font-medium);
108
+ line-height: var(--ui-line-height-tight);
109
+ }
110
+
111
+ .description {
112
+ color: var(--ui-text-muted);
113
+ font-size: var(--ui-font-size-sm);
114
+ line-height: var(--ui-line-height-tight);
115
+ }
116
+
117
+ .title > :slotted(a) {
118
+ color: inherit;
119
+ text-decoration: none;
120
+ }
121
+
122
+ .title > :slotted(a:focus-visible) {
123
+ outline: none;
124
+ box-shadow: none;
125
+ }
126
+
127
+ .title > :slotted(a[href])::after {
128
+ position: absolute;
129
+ inset: 0;
130
+ border-radius: inherit;
131
+ content: "";
132
+ }
133
+
134
+ /* An unused region takes no space. */
135
+ .leading:empty,
136
+ .description:empty,
137
+ .trailing:empty {
138
+ display: none;
139
+ }
140
+
141
+ /* A component that sets its display still honours the hidden attribute on its root. */
142
+ :host[hidden] {
143
+ display: none;
144
+ }
145
+ </style>
146
+ </template>
@@ -14,7 +14,7 @@ export default function controller(host) {
14
14
  const element = host.element;
15
15
  const document = element.ownerDocument;
16
16
  const overlayId = `ui-menu-${Math.random().toString(36).slice(2, 11)}`;
17
- const items = () => Array.from(element.querySelectorAll('[data-component~="ui-menu-item"]'));
17
+ const items = () => Array.from(element.querySelectorAll('[role="menuitem"]'));
18
18
  let anchor = null;
19
19
  let surface = null;
20
20
  let lastFor;
@@ -77,7 +77,7 @@ export default function controller(host) {
77
77
  host.dispatch("close", { open: false, reason: "action", trigger });
78
78
  host.state.internalOpen = false;
79
79
  };
80
- const onClick = (event) => select(event.target.closest?.('[data-component~="ui-menu-item"]'), triggerFor(event));
80
+ const onClick = (event) => select(event.target.closest?.('[role="menuitem"]'), triggerFor(event));
81
81
  const onKeydown = (event) => {
82
82
  if (event.key === "Escape") {
83
83
  event.preventDefault();
@@ -85,7 +85,7 @@ export default function controller(host) {
85
85
  return;
86
86
  }
87
87
  const enabled = items().filter((item) => !disabled(item));
88
- const target = event.target.closest?.('[data-component~="ui-menu-item"]');
88
+ const target = event.target.closest?.('[role="menuitem"]');
89
89
  const index = target ? enabled.indexOf(target) : -1;
90
90
  if (["Enter", " "].includes(event.key)) {
91
91
  if (index >= 0) {
@@ -0,0 +1,97 @@
1
+ <template component="ui-page-header">
2
+ <defs>
3
+ <prop name="align" type="start | center" default="start"
4
+ >Where the header sits. start runs along the page's start edge with actions at the end; center stacks everything centred, for a standalone card such as
5
+ sign-in.</prop>
6
+ </defs>
7
+ <header>
8
+ <span class="leading"><slot name="leading"></slot></span>
9
+ <span class="copy">
10
+ <span class="eyebrow"><slot name="eyebrow"></slot></span>
11
+ <h1 class="title"><slot></slot></h1>
12
+ <span class="description"><slot name="description"></slot></span>
13
+ </span>
14
+ <span class="actions"><slot name="actions"></slot></span>
15
+ </header>
16
+ <style>
17
+ /* The top of a view: its title (the page's one h1), with an optional icon or mark before it, a
18
+ short eyebrow above it, a line or two of description under it, and the view's own actions at
19
+ the end. Every view titles itself the same way, at the same size. */
20
+ :host {
21
+ display: flex;
22
+ flex-wrap: wrap;
23
+ align-items: center;
24
+ gap: var(--ui-space-3) var(--ui-space-4);
25
+ min-inline-size: 0;
26
+ }
27
+
28
+ .leading {
29
+ display: inline-flex;
30
+ flex: none;
31
+ align-self: center;
32
+ color: var(--ui-text-muted);
33
+ font-size: var(--ui-font-size-2xl);
34
+ }
35
+
36
+ .copy {
37
+ display: flex;
38
+ flex: 1 1 16rem;
39
+ flex-direction: column;
40
+ gap: var(--ui-space-1);
41
+ min-inline-size: 0;
42
+ }
43
+
44
+ .eyebrow {
45
+ color: var(--ui-text-muted);
46
+ font-size: var(--ui-font-size-xs);
47
+ font-weight: var(--ui-font-semibold);
48
+ letter-spacing: 0.06em;
49
+ text-transform: uppercase;
50
+ }
51
+
52
+ .title {
53
+ margin: 0;
54
+ color: var(--ui-text-primary);
55
+ font-size: var(--ui-font-size-2xl);
56
+ font-weight: var(--ui-font-bold, 700);
57
+ line-height: var(--ui-line-height-tight);
58
+ overflow-wrap: anywhere;
59
+ }
60
+
61
+ .description {
62
+ color: var(--ui-text-secondary);
63
+ font-size: var(--ui-font-size-md);
64
+ line-height: var(--ui-line-height);
65
+ }
66
+
67
+ .actions {
68
+ display: inline-flex;
69
+ flex: none;
70
+ flex-wrap: wrap;
71
+ align-items: center;
72
+ gap: var(--ui-space-2);
73
+ }
74
+
75
+ :host-state([align="center"]) {
76
+ flex-direction: column;
77
+ text-align: center;
78
+ }
79
+
80
+ :host-state([align="center"]) .copy {
81
+ align-items: center;
82
+ flex-basis: auto;
83
+ }
84
+
85
+ .leading:empty,
86
+ .eyebrow:empty,
87
+ .description:empty,
88
+ .actions:empty {
89
+ display: none;
90
+ }
91
+
92
+ /* A component that sets its display still honours the hidden attribute on its root. */
93
+ :host[hidden] {
94
+ display: none;
95
+ }
96
+ </style>
97
+ </template>
@@ -23,7 +23,7 @@ export default function controller(host) {
23
23
  input.addEventListener("change", onChange);
24
24
  const stopReset = afterFormReset(input, () => {
25
25
  // Inside a ui-radio-group the group's value decides, whichever reset runs first.
26
- if (input.closest('[data-component~="ui-radio-group"]')) return;
26
+ if (input.closest('[role="radiogroup"]')) return;
27
27
  host.state.internalChecked = Boolean(external);
28
28
  input.checked = Boolean(external);
29
29
  });
@@ -10,7 +10,7 @@ export default function controller(host) {
10
10
  const element = host.element;
11
11
  const name = String(host.state.name || `ui-radio-group-${++groups}`);
12
12
  const [trigger, stopTracking] = trackTrigger(host);
13
- const inputs = () => Array.from(element.querySelectorAll('[data-component~="ui-radio"] input[type="radio"]'));
13
+ const inputs = () => Array.from(element.querySelectorAll('input[type="radio"]'));
14
14
  let external = host.state.value;
15
15
  // A radio's own required survives until the group's required changes, since either one makes the
16
16
  // whole name group required.
@@ -0,0 +1,108 @@
1
+ <template component="ui-section">
2
+ <defs>
3
+ <prop name="heading" type="string" default=""
4
+ >The section's heading, an h2. Leave it empty for a section with no visible heading, and name the section with aria-label.</prop>
5
+ <prop name="variant" type="plain | card | divided" default="plain"
6
+ >How the section is set apart. plain is a heading and content in the flow of the page; card sits it on a bordered surface, for a settings group; divided
7
+ rules it off from the section before it, for a long form.</prop>
8
+ <prop name="tone" type="neutral | danger" default="neutral"
9
+ >danger marks a section of destructive actions (a danger zone): its heading and a card's border take the danger colour.</prop>
10
+ </defs>
11
+ <section>
12
+ <span class="head">
13
+ <span class="copy">
14
+ <h2 class="heading" :hidden="not heading"><template $value="heading"></template></h2>
15
+ <span class="description"><slot name="description"></slot></span>
16
+ </span>
17
+ <span class="actions"><slot name="actions"></slot></span>
18
+ </span>
19
+ <span class="body"><slot></slot></span>
20
+ </section>
21
+ <style>
22
+ /* A part of a view: a heading, an optional line under it, actions for the whole section at its
23
+ end, and the content. Sections space themselves out through the Stack around them. */
24
+ :host {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--ui-space-4);
28
+ min-inline-size: 0;
29
+ }
30
+
31
+ :host-state([variant="card"]) {
32
+ padding: var(--ui-space-5);
33
+ border: 1px solid var(--ui-border-default);
34
+ border-radius: var(--ui-radius-lg);
35
+ background: var(--ui-surface-raised);
36
+ }
37
+
38
+ :host-state([variant="divided"]) {
39
+ padding-block-start: var(--ui-space-6);
40
+ border-block-start: 1px solid var(--ui-border-default);
41
+ }
42
+
43
+ :host-state([variant="card"][tone="danger"]) {
44
+ border-color: var(--ui-danger);
45
+ }
46
+
47
+ .head {
48
+ display: flex;
49
+ flex-wrap: wrap;
50
+ align-items: flex-start;
51
+ justify-content: space-between;
52
+ gap: var(--ui-space-2) var(--ui-space-4);
53
+ }
54
+
55
+ .copy {
56
+ display: flex;
57
+ flex: 1 1 16rem;
58
+ flex-direction: column;
59
+ gap: var(--ui-space-1);
60
+ min-inline-size: 0;
61
+ }
62
+
63
+ .heading {
64
+ margin: 0;
65
+ color: var(--ui-text-primary);
66
+ font-size: var(--ui-font-size-lg);
67
+ font-weight: var(--ui-font-semibold);
68
+ line-height: var(--ui-line-height-tight);
69
+ }
70
+
71
+ :host-state([tone="danger"]) .heading {
72
+ color: var(--ui-danger);
73
+ }
74
+
75
+ .description {
76
+ color: var(--ui-text-secondary);
77
+ font-size: var(--ui-font-size-sm);
78
+ line-height: var(--ui-line-height);
79
+ }
80
+
81
+ .actions {
82
+ display: inline-flex;
83
+ flex-wrap: wrap;
84
+ gap: var(--ui-space-2);
85
+ }
86
+
87
+ .body {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: var(--ui-space-4);
91
+ min-inline-size: 0;
92
+ }
93
+
94
+ .description:empty,
95
+ .actions:empty {
96
+ display: none;
97
+ }
98
+
99
+ .head:has(.heading[hidden]):has(.description:empty):has(.actions:empty) {
100
+ display: none;
101
+ }
102
+
103
+ /* A component that sets its display still honours the hidden attribute on its root. */
104
+ :host[hidden] {
105
+ display: none;
106
+ }
107
+ </style>
108
+ </template>
@@ -0,0 +1,48 @@
1
+ <template component="ui-spinner">
2
+ <defs>
3
+ <prop name="label" type="string" default=""
4
+ >What is loading, for assistive technology ("Loading pages"). With a label the spinner is a status that is announced; without one it is decoration, for a
5
+ spinner beside visible text that already says so.</prop>
6
+ <prop name="size" type="sm | md" default="md">sm fits inside a button or a line of small text; md sits beside body text.</prop>
7
+ </defs>
8
+ <span
9
+ :role="{ true: 'status', false: null }[format('%s', label != '')]"
10
+ :aria-label="{ true: label, false: null }[format('%s', label != '')]"
11
+ :aria-hidden="{ true: null, false: 'true' }[format('%s', label != '')]"
12
+ >
13
+ <ui-icon name="loader"></ui-icon>
14
+ </span>
15
+ <style>
16
+ /* Something is on its way. It turns in the current text colour; with reduced motion it holds still,
17
+ and the label (or the text beside it) still says what is happening. */
18
+ :host {
19
+ display: inline-flex;
20
+ flex: none;
21
+ color: currentColor;
22
+ font-size: var(--ui-font-size-lg);
23
+ line-height: 1;
24
+ animation: ui-spinner-turn 0.8s linear infinite;
25
+ }
26
+
27
+ :host-state([size="sm"]) {
28
+ font-size: var(--ui-font-size-md);
29
+ }
30
+
31
+ @keyframes ui-spinner-turn {
32
+ to {
33
+ transform: rotate(1turn);
34
+ }
35
+ }
36
+
37
+ @media (prefers-reduced-motion: reduce) {
38
+ :host {
39
+ animation: none;
40
+ }
41
+ }
42
+
43
+ /* A component that sets its display still honours the hidden attribute on its root. */
44
+ :host[hidden] {
45
+ display: none;
46
+ }
47
+ </style>
48
+ </template>
@@ -0,0 +1,84 @@
1
+ <template component="ui-status-message">
2
+ <defs>
3
+ <prop name="kind" type="empty | loading | error" default="empty"
4
+ >What the view is saying. empty: there is nothing here yet (announced politely); loading: it is on its way, with a spinner (announced politely); error: it
5
+ could not be shown, in the danger tone (announced at once).</prop>
6
+ <prop name="heading" type="string" default="">A short heading above the message, such as "No pages yet". Optional.</prop>
7
+ <prop name="layout" type="inline | panel | fill" default="inline"
8
+ >Where it sits. inline takes its place in the view's flow; panel sits it on a dashed surface, for an empty area in a page; fill centres it in all the space
9
+ it is given, for a whole view that is loading or failed.</prop>
10
+ </defs>
11
+ <div :role="{ true: 'alert', false: 'status' }[format('%s', kind = 'error')]">
12
+ <ui-spinner size="sm" :hidden="kind != 'loading'"></ui-spinner>
13
+ <span class="copy">
14
+ <strong class="heading" :hidden="not heading"><template $value="heading"></template></strong>
15
+ <span class="message"><slot></slot></span>
16
+ </span>
17
+ <span class="action"><slot name="action"></slot></span>
18
+ </div>
19
+ <style>
20
+ /* The one way a view says it is loading, has nothing yet, or failed, with an optional next step.
21
+ Every view says it the same way, in the same place, at the same size. */
22
+ :host {
23
+ display: flex;
24
+ flex-wrap: wrap;
25
+ align-items: center;
26
+ gap: var(--ui-space-2) var(--ui-space-4);
27
+ padding-block: var(--ui-space-4);
28
+ color: var(--ui-text-muted);
29
+ font-size: var(--ui-font-size-md);
30
+ line-height: var(--ui-line-height);
31
+ }
32
+
33
+ :host-state([kind="error"]) {
34
+ color: var(--ui-danger);
35
+ }
36
+
37
+ :host-state([layout="panel"]) {
38
+ padding: var(--ui-space-6);
39
+ border: 1px dashed var(--ui-border-default);
40
+ border-radius: var(--ui-radius-lg);
41
+ }
42
+
43
+ :host-state([layout="fill"]) {
44
+ flex: 1;
45
+ flex-direction: column;
46
+ justify-content: center;
47
+ min-block-size: 50vh;
48
+ text-align: center;
49
+ }
50
+
51
+ .copy {
52
+ display: flex;
53
+ flex: 1 1 12rem;
54
+ flex-direction: column;
55
+ gap: var(--ui-space-1);
56
+ min-inline-size: 0;
57
+ }
58
+
59
+ :host-state([layout="fill"]) .copy {
60
+ flex: none;
61
+ align-items: center;
62
+ }
63
+
64
+ .heading {
65
+ color: var(--ui-text-primary);
66
+ font-weight: var(--ui-font-semibold);
67
+ }
68
+
69
+ .action {
70
+ display: inline-flex;
71
+ flex-wrap: wrap;
72
+ gap: var(--ui-space-2);
73
+ }
74
+
75
+ .action:empty {
76
+ display: none;
77
+ }
78
+
79
+ /* A component that sets its display still honours the hidden attribute on its root. */
80
+ :host[hidden] {
81
+ display: none;
82
+ }
83
+ </style>
84
+ </template>
@@ -0,0 +1,96 @@
1
+ <template component="ui-text">
2
+ <defs>
3
+ <prop name="size" type="inherit | xs | sm | md | lg" default="inherit"
4
+ >The step on the type scale. inherit (the default) keeps the size of the text around it; xs is for fine print, sm for supporting text such as help and
5
+ metadata, md is body text, and lg is a lead paragraph.</prop>
6
+ <prop name="tone" type="default | secondary | muted | accent | danger | success" default="default"
7
+ >The text's colour, by meaning. secondary is supporting prose such as a description; muted is quieter still, for metadata and hints; accent draws the eye;
8
+ danger states an error; success confirms one.</prop>
9
+ <prop name="weight" type="inherit | normal | medium | semibold" default="inherit"
10
+ >The weight. inherit keeps the surrounding weight; medium and semibold emphasise a name or a figure.</prop>
11
+ <prop name="variant" type="body | eyebrow | code" default="body"
12
+ >body is running text. eyebrow is a short, uppercase, tracked label that names what follows, such as a group of links; code sets a value to copy or read
13
+ exactly, such as an address, in the monospace face.</prop>
14
+ <prop name="truncate" type="boolean" default="false">Keeps the text on one line, ending in an ellipsis when it does not fit.</prop>
15
+ <prop name="align" type="start | center" default="start">Aligns the text within its line.</prop>
16
+ </defs>
17
+ <p as="p|span|div|small|strong|label"><slot></slot></p>
18
+ <style>
19
+ /* Running text, set on the type scale and in a tone that says what it is, so a view never styles
20
+ text of its own. The root is a paragraph unless as= names a span, div, small, strong, or label. */
21
+ :host {
22
+ margin: 0;
23
+ min-inline-size: 0;
24
+ color: var(--ui-text-primary);
25
+ text-align: start;
26
+ }
27
+
28
+ :host-state([size="xs"]) {
29
+ font-size: var(--ui-font-size-xs);
30
+ line-height: var(--ui-line-height);
31
+ }
32
+ :host-state([size="sm"]) {
33
+ font-size: var(--ui-font-size-sm);
34
+ line-height: var(--ui-line-height);
35
+ }
36
+ :host-state([size="md"]) {
37
+ font-size: var(--ui-font-size-md);
38
+ line-height: var(--ui-line-height);
39
+ }
40
+ :host-state([size="lg"]) {
41
+ font-size: var(--ui-font-size-lg);
42
+ line-height: var(--ui-line-height-relaxed);
43
+ }
44
+
45
+ :host-state([tone="secondary"]) {
46
+ color: var(--ui-text-secondary);
47
+ }
48
+ :host-state([tone="muted"]) {
49
+ color: var(--ui-text-muted);
50
+ }
51
+ :host-state([tone="accent"]) {
52
+ color: var(--ui-accent-active);
53
+ }
54
+ :host-state([tone="danger"]) {
55
+ color: var(--ui-danger);
56
+ }
57
+ :host-state([tone="success"]) {
58
+ color: var(--ui-success);
59
+ }
60
+
61
+ :host-state([weight="normal"]) {
62
+ font-weight: var(--ui-font-normal);
63
+ }
64
+ :host-state([weight="medium"]) {
65
+ font-weight: var(--ui-font-medium);
66
+ }
67
+ :host-state([weight="semibold"]) {
68
+ font-weight: var(--ui-font-semibold);
69
+ }
70
+
71
+ :host-state([variant="eyebrow"]) {
72
+ font-size: var(--ui-font-size-xs);
73
+ font-weight: var(--ui-font-semibold);
74
+ letter-spacing: 0.06em;
75
+ text-transform: uppercase;
76
+ }
77
+ :host-state([variant="code"]) {
78
+ font-family: var(--ui-font-family-mono);
79
+ }
80
+
81
+ :host-state([truncate]) {
82
+ overflow: hidden;
83
+ text-overflow: ellipsis;
84
+ white-space: nowrap;
85
+ }
86
+
87
+ :host-state([align="center"]) {
88
+ text-align: center;
89
+ }
90
+
91
+ /* A component that sets its display still honours the hidden attribute on its root. */
92
+ :host[hidden] {
93
+ display: none;
94
+ }
95
+ </style>
96
+ </template>
@@ -12,14 +12,14 @@ function itemId(item) {
12
12
  }
13
13
 
14
14
  function parentItem(item, tree) {
15
- const parent = item.parentElement?.closest('[data-component~="ui-tree-item"]') ?? null;
15
+ const parent = item.parentElement?.closest('[role="treeitem"]') ?? null;
16
16
  return parent && tree.contains(parent) ? parent : null;
17
17
  }
18
18
 
19
19
  export default function controller(host) {
20
20
  const element = host.element;
21
21
  const document = element.ownerDocument;
22
- const itemSelector = '[data-component~="ui-tree-item"]';
22
+ const itemSelector = '[role="treeitem"]';
23
23
  let source = null;
24
24
  let target = null;
25
25
  let position = null;
@@ -5,7 +5,7 @@ function triggerFor(event) {
5
5
  }
6
6
 
7
7
  function parentItem(element) {
8
- return element.parentElement?.closest('[data-component~="ui-tree-item"]') ?? null;
8
+ return element.parentElement?.closest('[role="treeitem"]') ?? null;
9
9
  }
10
10
 
11
11
  /** Synchronizes an inferred tree hierarchy after nested component lowering. */
@@ -23,13 +23,13 @@ export default function controller(host) {
23
23
  // Touch use enlarges rows and hides drag handles (see the template's styles).
24
24
  trackInputModality(element.ownerDocument);
25
25
  const childItems = () => Array.from(children?.children ?? [])
26
- .filter((child) => child.matches?.('[data-component~="ui-tree-item"]'));
26
+ .filter((child) => child.matches?.('[role="treeitem"]'));
27
27
  let lastExternalExpanded = Boolean(host.state.expanded);
28
28
  host.state.internalExpanded = lastExternalExpanded;
29
29
 
30
30
  const isContainer = () => Boolean(host.state.container) || childItems().length > 0;
31
31
  const updateLevel = () => {
32
- const tree = element.closest('[data-component~="ui-tree"]');
32
+ const tree = element.closest('[role="tree"]');
33
33
  let ancestor = parentItem(element);
34
34
  let level = 1;
35
35
  while (ancestor && tree?.contains(ancestor)) {