@ptbdynamic/plinth 0.1.1 → 0.2.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 (39) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/packages/css/dist/menu.css +245 -0
  4. package/packages/css/dist/page-header.css +124 -0
  5. package/packages/css/dist/plinth.css +833 -160
  6. package/packages/css/dist/section-menu.css +6 -5
  7. package/packages/css/dist/text-field.css +230 -0
  8. package/packages/css/dist/textarea.css +169 -0
  9. package/packages/css/dist/tokens.css +9 -1
  10. package/packages/react/dist/PlinthMenu.d.ts +12 -0
  11. package/packages/react/dist/PlinthMenuGroup.d.ts +9 -0
  12. package/packages/react/dist/PlinthMenuItem.d.ts +26 -0
  13. package/packages/react/dist/PlinthMenuSeparator.d.ts +4 -0
  14. package/packages/react/dist/PlinthPageHeader.d.ts +17 -0
  15. package/packages/react/dist/PlinthTextField.d.ts +24 -0
  16. package/packages/react/dist/PlinthTextarea.d.ts +24 -0
  17. package/packages/react/dist/index.d.ts +10 -1
  18. package/packages/react/dist/index.js +565 -366
  19. package/packages/react/dist/menu.d.ts +7 -0
  20. package/packages/react/dist/page-header.d.ts +5 -0
  21. package/packages/react/dist/text-field.d.ts +10 -0
  22. package/packages/tokens/dist/index.d.ts +13 -1
  23. package/packages/tokens/dist/index.js +131 -6
  24. package/packages/tokens/dist/tailwind-theme.css +2 -2
  25. package/packages/tokens/dist/tokens.css +9 -1
  26. package/packages/tokens/dist/tokens.json +116 -3
  27. package/packages/vue/dist/PlinthMenu.vue.d.ts +24 -0
  28. package/packages/vue/dist/{PlinthHeader.vue.d.ts → PlinthMenuGroup.vue.d.ts} +5 -7
  29. package/packages/vue/dist/PlinthMenuItem.vue.d.ts +34 -0
  30. package/packages/vue/dist/PlinthMenuSeparator.vue.d.ts +3 -0
  31. package/packages/vue/dist/PlinthPageHeader.vue.d.ts +27 -0
  32. package/packages/vue/dist/PlinthTextField.vue.d.ts +51 -0
  33. package/packages/vue/dist/PlinthTextarea.vue.d.ts +42 -0
  34. package/packages/vue/dist/index.d.ts +11 -2
  35. package/packages/vue/dist/index.js +661 -368
  36. package/packages/vue/dist/menu.d.ts +8 -0
  37. package/packages/vue/dist/page-header.d.ts +5 -0
  38. package/packages/vue/dist/text-field.d.ts +10 -0
  39. package/packages/react/dist/PlinthHeader.d.ts +0 -12
package/README.md CHANGED
@@ -91,7 +91,7 @@ The tokens are in `packages/tokens/tokens/`, in three tiers ([ADR 1](docs/adr/00
91
91
  | --------- | ------------------------------------------------------ | ------------------------------- | ----------------------------------- |
92
92
  | Primitive | `primitives.json` | `--plinth-ref-color-neutral-50` | semantic tokens only |
93
93
  | Semantic | `semantic.json`, `semantic.light.json`, `.dark.json` | `--plinth-color-surface` | components; projects override these |
94
- | Component | `component.json`, `component.light.json`, `.dark.json` | `--plinth-header-height` | one component |
94
+ | Component | `component.json`, `component.light.json`, `.dark.json` | `--plinth-page-header-height` | one component |
95
95
 
96
96
  The website has a page for each under **Foundations** (colour, typography, spacing and sizes,
97
97
  radii and elevation, layers and breakpoints, motion, the focus ring and component tokens), each
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ptbdynamic/plinth",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "The design system of BDynamic's projects: tokens, plain HTML and CSS components, and thin Blade, Vue and React layers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,245 @@
1
+ /*
2
+ * Menu: a short list of actions that opens from a button, in a popover anchored to it.
3
+ *
4
+ * <button type="button" class="plinth-button plinth-icon-button" popovertarget="ticket-actions"
5
+ * aria-label="More actions">…</button>
6
+ * <div class="plinth-menu" id="ticket-actions" popover="auto">
7
+ * <button type="button" class="plinth-menu-item" commandfor="ticket-actions"
8
+ * command="hide-popover">
9
+ * <span class="plinth-menu-icon" aria-hidden="true"><svg>…</svg></span>
10
+ * <span class="plinth-menu-label">Assign to me</span>
11
+ * <kbd class="plinth-menu-shortcut" aria-hidden="true">A</kbd>
12
+ * </button>
13
+ * <a class="plinth-menu-item" href="/tickets/42/print">
14
+ * <span class="plinth-menu-label">Print</span>
15
+ * </a>
16
+ * <hr class="plinth-menu-separator">
17
+ * <div class="plinth-menu-group" role="group" aria-label="Move to">
18
+ * <div class="plinth-menu-heading" aria-hidden="true">Move to</div>
19
+ * <button type="button" class="plinth-menu-item" commandfor="ticket-actions"
20
+ * command="hide-popover"><span class="plinth-menu-label">Archive</span></button>
21
+ * </div>
22
+ * <hr class="plinth-menu-separator">
23
+ * <button type="button" class="plinth-menu-item" commandfor="ticket-actions"
24
+ * command="hide-popover" data-tone="danger">
25
+ * <span class="plinth-menu-label">Delete</span>
26
+ * </button>
27
+ * </div>
28
+ *
29
+ * The root is a <div popover="auto"> with an id (required in Blade, Vue and React), which the
30
+ * app's button names in popovertarget (or commandfor with command="toggle-popover"). The browser
31
+ * opens and closes it (ADR 2): the top layer, a click outside or Escape closes it, focus goes back
32
+ * to the button, and the browser tells assistive technology whether the button's menu is
33
+ * expanded. The button is the app's own (an icon button or a button); it is not part of the menu.
34
+ *
35
+ * It is not role="menu". The ARIA menu pattern needs arrow keys and a roving focus, which would be
36
+ * script in every layer; this is a list of buttons and links, reached with Tab, as ADR 2 decides
37
+ * for menus of actions. So the button has no aria-haspopup, and the items are plain buttons and
38
+ * links that read as such.
39
+ *
40
+ * Parts, each its own component in Blade, Vue and React:
41
+ * - item: a <button type="button">, or an <a href> when it navigates. A button item closes its
42
+ * menu when pressed: commandfor="<the menu's id>" command="hide-popover", which the layers take
43
+ * from the menu around the item. A link closes the menu by leaving the page; an app that routes
44
+ * without loading a page closes it with hidePopover(). The icon is the project's SVG, decorative;
45
+ * the label is the item's name. The shortcut is a hint for the eye, aria-hidden; the app that
46
+ * has the shortcut adds aria-keyshortcuts to the item.
47
+ * - separator: an <hr> between items.
48
+ * - group: a <div role="group"> named by its heading (aria-label), with the heading shown above
49
+ * its items and hidden from assistive technology, which reads the group's name instead.
50
+ *
51
+ * Variants and states:
52
+ * - data-tone="danger" on an item that deletes or removes something: the danger text colour and a
53
+ * danger tint on hover. The label still says what it does; the colour is not the only signal.
54
+ * - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
55
+ * - hover only where the device can hover; pressed takes the sunken surface; the focus ring on
56
+ * :focus-visible, drawn inside the item (focus-ring.offset-inset).
57
+ * - open is the native one (:popover-open), never a class.
58
+ *
59
+ * Placement: below the button, starting at its start edge, through CSS anchor positioning (the
60
+ * popover's button is its implicit anchor). It takes the side of the button with more room, above
61
+ * or below, and is never taller than that room: a long menu scrolls. When it would leave the
62
+ * screen sideways it flips to the button's end edge. It is the same at every width; a phone gets no bottom sheet,
63
+ * since a menu is a few actions next to the button that opened them (a long list belongs in a
64
+ * sheet). A browser without anchor positioning shows the menu in the middle of the screen, where
65
+ * it still works. Script opens it with showPopover({ source: button }), so it has its anchor;
66
+ * without a source it has none, and sits in the screen's start corner.
67
+ *
68
+ * Size: --plinth-menu-min-width (192px) to --plinth-menu-max-width (320px), never wider than the
69
+ * screen less a margin, and never taller than the room it has. Items are --plinth-touch-target
70
+ * (40px) tall; with a mouse from 768px up, --plinth-control-md (32px). A long label wraps.
71
+ * It fades in and out on the motion tokens, which drop to 0ms under prefers-reduced-motion.
72
+ */
73
+
74
+ @layer plinth {
75
+ .plinth-menu {
76
+ /* The browser's popover rules centre it on the screen: the fallback without anchoring. */
77
+ box-sizing: border-box;
78
+ min-inline-size: min(var(--plinth-menu-min-width), 100vi - 2 * var(--plinth-space-2));
79
+ max-inline-size: min(var(--plinth-menu-max-width), 100vi - 2 * var(--plinth-space-2));
80
+ max-block-size: calc(100dvb - 2 * var(--plinth-space-2));
81
+ padding: var(--plinth-space-1);
82
+ overflow: hidden auto;
83
+ overscroll-behavior: contain;
84
+ font-family: var(--plinth-font-family-body);
85
+ font-size: var(--plinth-font-size-md);
86
+ line-height: var(--plinth-font-leading-tight);
87
+ color: var(--plinth-color-text);
88
+ background-color: var(--plinth-color-surface-raised);
89
+ border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
90
+ border-radius: var(--plinth-radius-container);
91
+ box-shadow: var(--plinth-elevation-2);
92
+ opacity: 0;
93
+ transition:
94
+ opacity var(--plinth-duration-fast) var(--plinth-easing-exit),
95
+ overlay var(--plinth-duration-fast) allow-discrete,
96
+ display var(--plinth-duration-fast) allow-discrete;
97
+ }
98
+
99
+ .plinth-menu:popover-open {
100
+ opacity: 1;
101
+ transition-timing-function: var(--plinth-easing-enter);
102
+ }
103
+
104
+ @starting-style {
105
+ .plinth-menu:popover-open {
106
+ opacity: 0;
107
+ }
108
+ }
109
+
110
+ @supports (position-area: block-end) {
111
+ .plinth-menu {
112
+ inset: auto;
113
+ max-block-size: calc(100% - 2 * var(--plinth-space-1));
114
+ margin-block: var(--plinth-space-1);
115
+ margin-inline: 0;
116
+ position-area: block-end span-inline-end;
117
+ position-try-order: most-block-size;
118
+ position-try-fallbacks:
119
+ flip-block,
120
+ flip-inline,
121
+ flip-block flip-inline;
122
+ }
123
+ }
124
+
125
+ .plinth-menu-item {
126
+ box-sizing: border-box;
127
+ display: flex;
128
+ gap: var(--plinth-space-2);
129
+ align-items: center;
130
+ inline-size: 100%;
131
+ min-block-size: var(--plinth-touch-target);
132
+ padding-block: var(--plinth-space-1);
133
+ padding-inline: var(--plinth-space-2);
134
+ margin: 0;
135
+ font: inherit;
136
+ color: var(--plinth-color-text);
137
+ text-align: start;
138
+ text-decoration: none;
139
+ cursor: pointer;
140
+ user-select: none;
141
+ background-color: transparent;
142
+ border: 0;
143
+ border-radius: var(--plinth-radius-control);
144
+ transition:
145
+ background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
146
+ color var(--plinth-duration-fast) var(--plinth-easing-standard);
147
+ }
148
+
149
+ .plinth-menu-item[data-tone='danger'] {
150
+ color: var(--plinth-color-danger-text);
151
+ }
152
+
153
+ @media (hover: hover) {
154
+ a.plinth-menu-item[href]:hover,
155
+ button.plinth-menu-item:enabled:hover {
156
+ background-color: var(--plinth-color-surface-hover);
157
+ }
158
+
159
+ a.plinth-menu-item[href][data-tone='danger']:hover,
160
+ button.plinth-menu-item:enabled[data-tone='danger']:hover {
161
+ background-color: var(--plinth-color-danger-surface);
162
+ }
163
+ }
164
+
165
+ a.plinth-menu-item[href]:active,
166
+ button.plinth-menu-item:enabled:active {
167
+ background-color: var(--plinth-color-surface-sunken);
168
+ }
169
+
170
+ .plinth-menu-item:focus-visible {
171
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
172
+ outline-offset: var(--plinth-focus-ring-offset-inset);
173
+ }
174
+
175
+ .plinth-menu-item:disabled,
176
+ .plinth-menu-item[aria-disabled='true'] {
177
+ cursor: not-allowed;
178
+ opacity: var(--plinth-opacity-disabled);
179
+ }
180
+
181
+ .plinth-menu-icon {
182
+ display: inline-flex;
183
+ flex: none;
184
+ inline-size: var(--plinth-size-icon-sm);
185
+ block-size: var(--plinth-size-icon-sm);
186
+ color: var(--plinth-color-text-muted);
187
+ }
188
+
189
+ .plinth-menu-item[data-tone='danger'] .plinth-menu-icon {
190
+ color: inherit;
191
+ }
192
+
193
+ .plinth-menu-icon > svg {
194
+ inline-size: 100%;
195
+ block-size: 100%;
196
+ }
197
+
198
+ .plinth-menu-label {
199
+ flex: 1 1 auto;
200
+ min-inline-size: 0;
201
+ overflow-wrap: anywhere;
202
+ }
203
+
204
+ .plinth-menu-shortcut {
205
+ flex: none;
206
+ padding-inline-start: var(--plinth-space-4);
207
+ margin-inline-start: auto;
208
+ font-family: var(--plinth-font-family-mono);
209
+ font-size: var(--plinth-font-size-xs);
210
+ color: var(--plinth-color-text-subtle);
211
+ }
212
+
213
+ .plinth-menu-separator {
214
+ block-size: 0;
215
+ margin-block: var(--plinth-space-1);
216
+ margin-inline: calc(-1 * var(--plinth-space-1));
217
+ border: 0;
218
+ border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
219
+ }
220
+
221
+ .plinth-menu-group {
222
+ margin: 0;
223
+ }
224
+
225
+ .plinth-menu-heading {
226
+ padding-block: var(--plinth-space-2) var(--plinth-space-1);
227
+ padding-inline: var(--plinth-space-2);
228
+ font-size: var(--plinth-font-size-xs);
229
+ font-weight: var(--plinth-font-weight-medium);
230
+ color: var(--plinth-color-text-subtle);
231
+ }
232
+
233
+ @media (min-width: 768px) and (pointer: fine) {
234
+ .plinth-menu-item {
235
+ min-block-size: var(--plinth-control-md);
236
+ }
237
+ }
238
+
239
+ @media (forced-colors: active) {
240
+ .plinth-menu-item:disabled,
241
+ .plinth-menu-item[aria-disabled='true'] {
242
+ color: GrayText;
243
+ }
244
+ }
245
+ }
@@ -0,0 +1,124 @@
1
+ /*
2
+ * Page header: the bar at the top of a page, with a leading action, the page's heading and
3
+ * trailing actions. On a phone it is the screen's top bar; in an app shell it heads the page beside
4
+ * the sidebar, under the app bar. It sticks to the top as the content scrolls under it.
5
+ *
6
+ * <header class="plinth-page-header" data-top>
7
+ * <div class="plinth-page-header-leading">
8
+ * <a class="plinth-button plinth-icon-button" href="/inbox" data-variant="ghost"
9
+ * data-size="md" aria-label="Back">
10
+ * <span class="plinth-icon" data-size="md" data-mirror aria-hidden="true"><svg>…</svg></span>
11
+ * </a>
12
+ * </div>
13
+ * <h1 class="plinth-page-header-heading">Inbox</h1>
14
+ * <div class="plinth-page-header-actions">
15
+ * <button type="button" class="plinth-button plinth-icon-button" data-variant="ghost"
16
+ * data-size="md" aria-label="Search">…</button>
17
+ * </div>
18
+ * </header>
19
+ *
20
+ * The root is a <header>. At the top of the page (not inside <main>, <article>, <section>, <aside>
21
+ * or <nav>) it is the page's banner landmark, so a page has one there; inside <main>, as in an app
22
+ * shell, it is no landmark, only the page's header. The heading is required: an <h1> by default, or
23
+ * <h2> to <h6> (level) when the page sits beside another page's <h1>, such as an open ticket beside
24
+ * the list on a wide screen. The level doesn't change the look. The heading stays on one line and
25
+ * ends in an ellipsis when it doesn't fit.
26
+ *
27
+ * The leading part holds one action, usually an icon button: a menu button that opens a sheet
28
+ * (commandfor and command="show-modal") on a top-level page, or a back link (with data-mirror on
29
+ * its icon so it points the other way in right-to-left text) on a page under it. The actions part
30
+ * holds the trailing actions: at most three icon buttons (put the rest in a menu), or a primary
31
+ * button such as New ticket. Both are optional, and printed only when they have content. Every
32
+ * icon button needs its own name (label). The page header needs button.css and icon.css for them.
33
+ *
34
+ * Variant (the app sets it, the page header doesn't guess):
35
+ * - data-top: the page header is the screen's top bar, at the very top of a phone's screen. It
36
+ * adds env(safe-area-inset-top) above its height, which pads it below the status bar and the
37
+ * notch on a phone that draws under them (viewport-fit=cover). Leave it off under an app bar.
38
+ *
39
+ * Its height is the page-header.height component token (56px). The inline padding clears the
40
+ * notch on a phone held sideways. It is sticky on the layer.sticky z-index, so it sits above the
41
+ * page and below sheets and menus (the top layer), and it sticks --plinth-page-header-offset from
42
+ * the top: 0 by default, or the app bar's height in a shell that has one. Content scrolls under
43
+ * it, so the app sets scroll-padding-block-start on its scrolling element (the offset plus the
44
+ * height, plus env(safe-area-inset-top) with data-top) to keep a focused element or an anchor's
45
+ * target from landing under the page header.
46
+ *
47
+ * The actions sit var(--plinth-space-2) apart, so on a touch screen the 40px hit areas of 32px
48
+ * icon buttons don't overlap.
49
+ *
50
+ * The app owns the state: the page header has no states of its own. What the actions do is the
51
+ * app's (a link, a commandfor, wire:click, @click on the icon button).
52
+ */
53
+
54
+ @layer plinth {
55
+ .plinth-page-header {
56
+ position: sticky;
57
+ inset-block-start: var(--plinth-page-header-offset);
58
+ z-index: var(--plinth-layer-sticky);
59
+ box-sizing: border-box;
60
+ display: flex;
61
+ gap: var(--plinth-space-2);
62
+ align-items: center;
63
+ min-block-size: var(--plinth-page-header-height);
64
+ padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
65
+ max(var(--plinth-space-2), env(safe-area-inset-right));
66
+ font-family: var(--plinth-font-family-body);
67
+ color: var(--plinth-color-text);
68
+ background-color: var(--plinth-color-surface);
69
+ border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
70
+ }
71
+
72
+ .plinth-page-header:dir(rtl) {
73
+ padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
74
+ max(var(--plinth-space-2), env(safe-area-inset-left));
75
+ }
76
+
77
+ /* The screen's top bar clears the status bar and the notch. */
78
+ .plinth-page-header[data-top] {
79
+ min-block-size: calc(var(--plinth-page-header-height) + env(safe-area-inset-top));
80
+ padding-block-start: env(safe-area-inset-top);
81
+ }
82
+
83
+ .plinth-page-header-leading,
84
+ .plinth-page-header-actions {
85
+ display: flex;
86
+ flex: none;
87
+ gap: var(--plinth-space-2);
88
+ align-items: center;
89
+ }
90
+
91
+ .plinth-page-header-heading {
92
+ flex: 1 1 auto;
93
+ min-inline-size: 0;
94
+ margin: 0;
95
+ overflow: hidden;
96
+ text-overflow: ellipsis;
97
+ font-size: var(--plinth-font-size-lg);
98
+ font-weight: var(--plinth-font-weight-semibold);
99
+ line-height: var(--plinth-font-leading-tight);
100
+ letter-spacing: var(--plinth-font-tracking-tight);
101
+ white-space: nowrap;
102
+ }
103
+
104
+ /* Without a leading action or trailing actions, the heading keeps the text's distance from the edge. */
105
+ .plinth-page-header-heading:first-child {
106
+ margin-inline-start: var(--plinth-space-2);
107
+ }
108
+
109
+ .plinth-page-header-heading:last-child {
110
+ margin-inline-end: var(--plinth-space-2);
111
+ }
112
+
113
+ @media (min-width: 768px) {
114
+ .plinth-page-header {
115
+ padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
116
+ max(var(--plinth-space-4), env(safe-area-inset-right));
117
+ }
118
+
119
+ .plinth-page-header:dir(rtl) {
120
+ padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
121
+ max(var(--plinth-space-4), env(safe-area-inset-left));
122
+ }
123
+ }
124
+ }