@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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/css/dist/menu.css +245 -0
- package/packages/css/dist/page-header.css +124 -0
- package/packages/css/dist/plinth.css +833 -160
- package/packages/css/dist/section-menu.css +6 -5
- package/packages/css/dist/text-field.css +230 -0
- package/packages/css/dist/textarea.css +169 -0
- package/packages/css/dist/tokens.css +9 -1
- package/packages/react/dist/PlinthMenu.d.ts +12 -0
- package/packages/react/dist/PlinthMenuGroup.d.ts +9 -0
- package/packages/react/dist/PlinthMenuItem.d.ts +26 -0
- package/packages/react/dist/PlinthMenuSeparator.d.ts +4 -0
- package/packages/react/dist/PlinthPageHeader.d.ts +17 -0
- package/packages/react/dist/PlinthTextField.d.ts +24 -0
- package/packages/react/dist/PlinthTextarea.d.ts +24 -0
- package/packages/react/dist/index.d.ts +10 -1
- package/packages/react/dist/index.js +565 -366
- package/packages/react/dist/menu.d.ts +7 -0
- package/packages/react/dist/page-header.d.ts +5 -0
- package/packages/react/dist/text-field.d.ts +10 -0
- package/packages/tokens/dist/index.d.ts +13 -1
- package/packages/tokens/dist/index.js +131 -6
- package/packages/tokens/dist/tailwind-theme.css +2 -2
- package/packages/tokens/dist/tokens.css +9 -1
- package/packages/tokens/dist/tokens.json +116 -3
- package/packages/vue/dist/PlinthMenu.vue.d.ts +24 -0
- package/packages/vue/dist/{PlinthHeader.vue.d.ts → PlinthMenuGroup.vue.d.ts} +5 -7
- package/packages/vue/dist/PlinthMenuItem.vue.d.ts +34 -0
- package/packages/vue/dist/PlinthMenuSeparator.vue.d.ts +3 -0
- package/packages/vue/dist/PlinthPageHeader.vue.d.ts +27 -0
- package/packages/vue/dist/PlinthTextField.vue.d.ts +51 -0
- package/packages/vue/dist/PlinthTextarea.vue.d.ts +42 -0
- package/packages/vue/dist/index.d.ts +11 -2
- package/packages/vue/dist/index.js +661 -368
- package/packages/vue/dist/menu.d.ts +8 -0
- package/packages/vue/dist/page-header.d.ts +5 -0
- package/packages/vue/dist/text-field.d.ts +10 -0
- package/packages/react/dist/PlinthHeader.d.ts +0 -12
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Section menu: the row under the header that switches between the sections of a page.
|
|
2
|
+
* Section menu: the row under the page header that switches between the sections of a page.
|
|
3
3
|
*
|
|
4
4
|
* <nav class="plinth-section-menu" aria-label="Project">
|
|
5
5
|
* <a class="plinth-section-menu-item" href="/overview" aria-current="page">
|
|
@@ -33,15 +33,16 @@
|
|
|
33
33
|
* - hover only where the device can hover; the focus ring on :focus-visible.
|
|
34
34
|
*
|
|
35
35
|
* Layout: a row --plinth-section-menu-height (56px) tall, on the surface with a divider below,
|
|
36
|
-
* the same inline padding as the header and clear of the notch on a phone held sideways. Its
|
|
36
|
+
* the same inline padding as the page header and clear of the notch on a phone held sideways. Its
|
|
37
37
|
* items are --plinth-section-menu-item-height (40px) tall, never less than the 40px touch
|
|
38
38
|
* target, and at least 40px wide. The row never wraps: when the items don't fit it scrolls
|
|
39
39
|
* sideways and snaps to them, with a thin scrollbar for a mouse and none on a touch screen; the
|
|
40
40
|
* row's block padding keeps the focus ring inside it. The app scrolls a current item that starts
|
|
41
41
|
* out of view into view.
|
|
42
|
-
* Placement is the app's: put it under the header, or make it sticky below it with
|
|
43
|
-
* position: sticky, inset-block-start: calc(var(--plinth-header-
|
|
44
|
-
* env(safe-area-inset-top)
|
|
42
|
+
* Placement is the app's: put it under the page header, or make it sticky below it with
|
|
43
|
+
* position: sticky, inset-block-start: calc(var(--plinth-page-header-offset) +
|
|
44
|
+
* var(--plinth-page-header-height)), plus env(safe-area-inset-top) under the screen's top bar,
|
|
45
|
+
* and z-index: var(--plinth-layer-sticky).
|
|
45
46
|
*/
|
|
46
47
|
|
|
47
48
|
@layer plinth {
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Text field: one line of text, with its label, an optional description and an error.
|
|
3
|
+
*
|
|
4
|
+
* <div class="plinth-text-field" data-size="md">
|
|
5
|
+
* <label class="plinth-text-field-label" for="email">Email</label>
|
|
6
|
+
* <div class="plinth-text-field-control">
|
|
7
|
+
* <span class="plinth-icon" data-size="sm" aria-hidden="true"><svg>…</svg></span>
|
|
8
|
+
* <input type="email" class="plinth-text-field-input" id="email"
|
|
9
|
+
* aria-describedby="email-description email-error" aria-invalid="true">
|
|
10
|
+
* <span class="plinth-text-field-trailing">…</span>
|
|
11
|
+
* </div>
|
|
12
|
+
* <p class="plinth-text-field-description" id="email-description">For receipts only.</p>
|
|
13
|
+
* <p class="plinth-text-field-error" id="email-error">Enter an email address.</p>
|
|
14
|
+
* </div>
|
|
15
|
+
*
|
|
16
|
+
* The input needs an id: the label points at it with for, and the description's and error's ids
|
|
17
|
+
* are the input's id with -description and -error. The icon, the trailing slot, the description
|
|
18
|
+
* and the error are optional; aria-describedby lists the description, then the error, and is left
|
|
19
|
+
* out when there is neither.
|
|
20
|
+
*
|
|
21
|
+
* Attributes go to the input, not the root (name, value, placeholder, autocomplete, required,
|
|
22
|
+
* wire:model, x-model, data-*, aria-*), so the app talks to the input; only a project's class is
|
|
23
|
+
* added to the root's.
|
|
24
|
+
*
|
|
25
|
+
* - type (on the input): text by default; email, url, tel, password, number or search. A search
|
|
26
|
+
* field takes a search icon and, in the trailing slot, a clear button (a small ghost icon
|
|
27
|
+
* button) that the app shows when there is text and wires up. With a trailing slot, the
|
|
28
|
+
* browser's own clear button is hidden, so there is only one.
|
|
29
|
+
* - the leading icon is 16px and decorative (aria-hidden); the label names the field.
|
|
30
|
+
* - the trailing slot holds one small control or hint, in the room the field keeps at its end
|
|
31
|
+
* (--plinth-text-field-trailing-size, 32px): a small icon button, or a keyboard hint as <kbd>.
|
|
32
|
+
* - data-label="hidden" on the root hides the label visually; screen readers still read it. Use it
|
|
33
|
+
* only where the field's purpose is plain without it, such as a search box with its icon.
|
|
34
|
+
*
|
|
35
|
+
* Sizes (data-size): md 32px (default, also when missing) and sm 28px. On a touch screen both are
|
|
36
|
+
* 40px tall. The text is 16px on a phone, so iOS doesn't zoom into the field, and from 768px
|
|
37
|
+
* up it is 14px (md) or 13px (sm).
|
|
38
|
+
*
|
|
39
|
+
* States:
|
|
40
|
+
* - error: aria-invalid="true" on the input and the error message under it; the edge turns
|
|
41
|
+
* danger and thick, so it isn't only a colour, and the message says what to fix.
|
|
42
|
+
* - disabled: `disabled` on the input; the field is faded and can't be focused.
|
|
43
|
+
* - read-only: `readonly` on the input; it keeps its text and focus, without the field's fill
|
|
44
|
+
* and with a dashed edge.
|
|
45
|
+
* - the focus ring on the input's :focus-visible.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
@layer plinth {
|
|
49
|
+
.plinth-text-field {
|
|
50
|
+
--plinth-text-field-height: var(--plinth-control-md);
|
|
51
|
+
--plinth-text-field-text: var(--plinth-font-size-md);
|
|
52
|
+
|
|
53
|
+
display: grid;
|
|
54
|
+
gap: var(--plinth-space-2);
|
|
55
|
+
min-inline-size: 0;
|
|
56
|
+
font-family: var(--plinth-font-family-body);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.plinth-text-field[data-size='sm'] {
|
|
60
|
+
--plinth-text-field-height: var(--plinth-control-sm);
|
|
61
|
+
--plinth-text-field-text: var(--plinth-font-size-sm);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.plinth-text-field-label {
|
|
65
|
+
font-size: var(--plinth-font-size-sm);
|
|
66
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
67
|
+
line-height: var(--plinth-font-leading-normal);
|
|
68
|
+
color: var(--plinth-color-text-muted);
|
|
69
|
+
overflow-wrap: anywhere;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Hidden from sight, still read by screen readers. */
|
|
73
|
+
.plinth-text-field[data-label='hidden'] .plinth-text-field-label {
|
|
74
|
+
position: absolute;
|
|
75
|
+
inline-size: 1px;
|
|
76
|
+
block-size: 1px;
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
white-space: nowrap;
|
|
79
|
+
clip-path: inset(50%);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.plinth-text-field-control {
|
|
83
|
+
position: relative;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
min-inline-size: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.plinth-text-field-input {
|
|
90
|
+
box-sizing: border-box;
|
|
91
|
+
flex: 1;
|
|
92
|
+
min-inline-size: 0;
|
|
93
|
+
min-block-size: var(--plinth-text-field-height);
|
|
94
|
+
padding-block: 0;
|
|
95
|
+
padding-inline: var(--plinth-space-3);
|
|
96
|
+
margin: 0;
|
|
97
|
+
font-family: inherit;
|
|
98
|
+
font-size: var(--plinth-font-size-field);
|
|
99
|
+
line-height: var(--plinth-font-leading-tight);
|
|
100
|
+
color: var(--plinth-color-text);
|
|
101
|
+
appearance: none;
|
|
102
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
103
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
104
|
+
border-radius: var(--plinth-radius-control);
|
|
105
|
+
transition: border-color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.plinth-text-field-input::placeholder {
|
|
109
|
+
color: var(--plinth-color-text-subtle);
|
|
110
|
+
opacity: 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@media (hover: hover) {
|
|
114
|
+
.plinth-text-field-input:hover:not(:disabled, :read-only, [aria-invalid='true']) {
|
|
115
|
+
border-color: var(--plinth-color-text-subtle);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.plinth-text-field-input:focus-visible {
|
|
120
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
121
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.plinth-text-field-input[aria-invalid='true'] {
|
|
125
|
+
border-color: var(--plinth-color-danger);
|
|
126
|
+
border-width: var(--plinth-border-width-thick);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.plinth-text-field-input:disabled {
|
|
130
|
+
cursor: not-allowed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Without the fill, and with a dashed edge, so it doesn't read as a field to type in. */
|
|
134
|
+
.plinth-text-field-input:read-only:not(:disabled) {
|
|
135
|
+
background-color: transparent;
|
|
136
|
+
border-style: dashed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.plinth-text-field-control:has(.plinth-text-field-input:disabled) {
|
|
140
|
+
opacity: var(--plinth-opacity-disabled);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* The leading icon sits over the input's start, which keeps room for it. */
|
|
144
|
+
.plinth-text-field-control > .plinth-icon {
|
|
145
|
+
position: absolute;
|
|
146
|
+
inset-inline-start: var(--plinth-space-3);
|
|
147
|
+
color: var(--plinth-color-text-subtle);
|
|
148
|
+
pointer-events: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.plinth-text-field-control:has(> .plinth-icon) > .plinth-text-field-input {
|
|
152
|
+
padding-inline-start: calc(
|
|
153
|
+
var(--plinth-space-3) + var(--plinth-size-icon-sm) + var(--plinth-space-2)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.plinth-text-field-trailing {
|
|
158
|
+
position: absolute;
|
|
159
|
+
inset-inline-end: var(--plinth-space-1);
|
|
160
|
+
display: flex;
|
|
161
|
+
gap: var(--plinth-space-1);
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: center;
|
|
164
|
+
min-inline-size: var(--plinth-text-field-trailing-size);
|
|
165
|
+
color: var(--plinth-color-text-subtle);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.plinth-text-field-control:has(> .plinth-text-field-trailing) > .plinth-text-field-input {
|
|
169
|
+
padding-inline-end: calc(var(--plinth-space-1) + var(--plinth-text-field-trailing-size));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* With its own clear button in the trailing slot, the browser's is one too many. */
|
|
173
|
+
.plinth-text-field-control:has(> .plinth-text-field-trailing)
|
|
174
|
+
> .plinth-text-field-input::-webkit-search-cancel-button {
|
|
175
|
+
display: none;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.plinth-text-field-trailing kbd {
|
|
179
|
+
box-sizing: border-box;
|
|
180
|
+
min-inline-size: var(--plinth-size-icon-md);
|
|
181
|
+
padding-block: 0;
|
|
182
|
+
padding-inline: var(--plinth-space-1);
|
|
183
|
+
font-family: var(--plinth-font-family-mono);
|
|
184
|
+
font-size: var(--plinth-font-size-xs);
|
|
185
|
+
line-height: var(--plinth-font-leading-normal);
|
|
186
|
+
color: var(--plinth-color-text-muted);
|
|
187
|
+
text-align: center;
|
|
188
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
189
|
+
border-radius: var(--plinth-radius-control);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.plinth-text-field-description,
|
|
193
|
+
.plinth-text-field-error {
|
|
194
|
+
margin: 0;
|
|
195
|
+
font-size: var(--plinth-font-size-sm);
|
|
196
|
+
line-height: var(--plinth-font-leading-normal);
|
|
197
|
+
overflow-wrap: anywhere;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.plinth-text-field-description {
|
|
201
|
+
color: var(--plinth-color-text-muted);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.plinth-text-field-error {
|
|
205
|
+
color: var(--plinth-color-danger-text);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@media (pointer: coarse) {
|
|
209
|
+
.plinth-text-field-input {
|
|
210
|
+
min-block-size: var(--plinth-touch-target);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@media (min-width: 768px) {
|
|
215
|
+
.plinth-text-field-input {
|
|
216
|
+
font-size: var(--plinth-text-field-text);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@media (forced-colors: active) {
|
|
221
|
+
.plinth-text-field-control:has(.plinth-text-field-input:disabled) {
|
|
222
|
+
color: GrayText;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.plinth-text-field-input:disabled {
|
|
226
|
+
color: GrayText;
|
|
227
|
+
border-color: GrayText;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Textarea: several lines of text, with its label, an optional description and an error. With
|
|
3
|
+
* grow, it grows with its content.
|
|
4
|
+
*
|
|
5
|
+
* <div class="plinth-textarea">
|
|
6
|
+
* <label class="plinth-textarea-label" for="reply">Reply</label>
|
|
7
|
+
* <textarea class="plinth-textarea-input" id="reply" rows="3"
|
|
8
|
+
* aria-describedby="reply-description reply-error" aria-invalid="true"></textarea>
|
|
9
|
+
* <p class="plinth-textarea-description" id="reply-description">The customer sees it.</p>
|
|
10
|
+
* <p class="plinth-textarea-error" id="reply-error">Write a reply first.</p>
|
|
11
|
+
* </div>
|
|
12
|
+
*
|
|
13
|
+
* The textarea needs an id: the label points at it with for, and the description's and error's
|
|
14
|
+
* ids are the textarea's id with -description and -error. The description and the error are
|
|
15
|
+
* optional; aria-describedby lists the description, then the error, and is left out when there is
|
|
16
|
+
* neither. rows is 3 by default.
|
|
17
|
+
*
|
|
18
|
+
* Attributes go to the textarea, not the root (name, placeholder, maxlength, required,
|
|
19
|
+
* wire:model, x-model, data-*, aria-*), so the app talks to the textarea; only a project's class
|
|
20
|
+
* is added to the root's.
|
|
21
|
+
*
|
|
22
|
+
* - grow: data-grow on the textarea, with its rows and maximum as custom properties:
|
|
23
|
+
* <textarea class="plinth-textarea-input" id="reply" rows="3" data-grow
|
|
24
|
+
* style="--plinth-textarea-rows: 3; --plinth-textarea-max-rows: 8"></textarea>
|
|
25
|
+
* It starts at rows lines and grows with its content (field-sizing: content) up to max-rows
|
|
26
|
+
* (8 by default), then scrolls. Where the browser has no field-sizing it keeps rows lines and
|
|
27
|
+
* the person can resize it, as without grow.
|
|
28
|
+
* - data-label="hidden" on the root hides the label visually; screen readers still read it. Use it
|
|
29
|
+
* only where the field's purpose is plain without it, such as a reply box under a thread.
|
|
30
|
+
*
|
|
31
|
+
* The text is 16px on a phone, so iOS doesn't zoom into the field, and 14px from 768px up.
|
|
32
|
+
*
|
|
33
|
+
* States:
|
|
34
|
+
* - error: aria-invalid="true" on the textarea and the error message under it; the edge turns
|
|
35
|
+
* danger and thick, so it isn't only a colour, and the message says what to fix.
|
|
36
|
+
* - disabled: `disabled` on the textarea; the field is faded and can't be focused.
|
|
37
|
+
* - read-only: `readonly` on the textarea; it keeps its text and focus, without the field's fill
|
|
38
|
+
* and with a dashed edge.
|
|
39
|
+
* - the focus ring on the textarea's :focus-visible.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
@layer plinth {
|
|
43
|
+
.plinth-textarea {
|
|
44
|
+
display: grid;
|
|
45
|
+
gap: var(--plinth-space-2);
|
|
46
|
+
min-inline-size: 0;
|
|
47
|
+
font-family: var(--plinth-font-family-body);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.plinth-textarea-label {
|
|
51
|
+
font-size: var(--plinth-font-size-sm);
|
|
52
|
+
font-weight: var(--plinth-font-weight-medium);
|
|
53
|
+
line-height: var(--plinth-font-leading-normal);
|
|
54
|
+
color: var(--plinth-color-text-muted);
|
|
55
|
+
overflow-wrap: anywhere;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Hidden from sight, still read by screen readers. */
|
|
59
|
+
.plinth-textarea[data-label='hidden'] .plinth-textarea-label {
|
|
60
|
+
position: absolute;
|
|
61
|
+
inline-size: 1px;
|
|
62
|
+
block-size: 1px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
white-space: nowrap;
|
|
65
|
+
clip-path: inset(50%);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.plinth-textarea-input {
|
|
69
|
+
box-sizing: border-box;
|
|
70
|
+
inline-size: 100%;
|
|
71
|
+
min-block-size: var(--plinth-touch-target);
|
|
72
|
+
padding-block: var(--plinth-space-2);
|
|
73
|
+
padding-inline: var(--plinth-space-3);
|
|
74
|
+
margin: 0;
|
|
75
|
+
font-family: inherit;
|
|
76
|
+
font-size: var(--plinth-font-size-field);
|
|
77
|
+
line-height: var(--plinth-font-leading-normal);
|
|
78
|
+
color: var(--plinth-color-text);
|
|
79
|
+
resize: vertical;
|
|
80
|
+
background-color: var(--plinth-color-surface-sunken);
|
|
81
|
+
border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
|
|
82
|
+
border-radius: var(--plinth-radius-control);
|
|
83
|
+
transition: border-color var(--plinth-duration-fast) var(--plinth-easing-standard);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.plinth-textarea-input::placeholder {
|
|
87
|
+
color: var(--plinth-color-text-subtle);
|
|
88
|
+
opacity: 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media (hover: hover) {
|
|
92
|
+
.plinth-textarea-input:hover:not(:disabled, :read-only, [aria-invalid='true']) {
|
|
93
|
+
border-color: var(--plinth-color-text-subtle);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.plinth-textarea-input:focus-visible {
|
|
98
|
+
outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
|
|
99
|
+
outline-offset: var(--plinth-focus-ring-offset);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.plinth-textarea-input[aria-invalid='true'] {
|
|
103
|
+
border-color: var(--plinth-color-danger);
|
|
104
|
+
border-width: var(--plinth-border-width-thick);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.plinth-textarea-input:disabled {
|
|
108
|
+
cursor: not-allowed;
|
|
109
|
+
resize: none;
|
|
110
|
+
opacity: var(--plinth-opacity-disabled);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Without the fill, and with a dashed edge, so it doesn't read as a field to type in. */
|
|
114
|
+
.plinth-textarea-input:read-only:not(:disabled) {
|
|
115
|
+
background-color: transparent;
|
|
116
|
+
border-style: dashed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* Grow: from rows lines to max-rows lines (in the textarea's own line height, with its padding
|
|
121
|
+
* and edge), then it scrolls.
|
|
122
|
+
*/
|
|
123
|
+
@supports (field-sizing: content) {
|
|
124
|
+
.plinth-textarea-input[data-grow] {
|
|
125
|
+
field-sizing: content;
|
|
126
|
+
min-block-size: max(
|
|
127
|
+
var(--plinth-touch-target),
|
|
128
|
+
calc(
|
|
129
|
+
var(--plinth-textarea-rows, 3) * 1lh + 2 * var(--plinth-space-2) + 2 *
|
|
130
|
+
var(--plinth-border-width-thin)
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
max-block-size: calc(
|
|
134
|
+
var(--plinth-textarea-max-rows, 8) * 1lh + 2 * var(--plinth-space-2) + 2 *
|
|
135
|
+
var(--plinth-border-width-thin)
|
|
136
|
+
);
|
|
137
|
+
resize: none;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.plinth-textarea-description,
|
|
142
|
+
.plinth-textarea-error {
|
|
143
|
+
margin: 0;
|
|
144
|
+
font-size: var(--plinth-font-size-sm);
|
|
145
|
+
line-height: var(--plinth-font-leading-normal);
|
|
146
|
+
overflow-wrap: anywhere;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.plinth-textarea-description {
|
|
150
|
+
color: var(--plinth-color-text-muted);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.plinth-textarea-error {
|
|
154
|
+
color: var(--plinth-color-danger-text);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@media (min-width: 768px) {
|
|
158
|
+
.plinth-textarea-input {
|
|
159
|
+
font-size: var(--plinth-font-size-md);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@media (forced-colors: active) {
|
|
164
|
+
.plinth-textarea-input:disabled {
|
|
165
|
+
color: GrayText;
|
|
166
|
+
border-color: GrayText;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -87,6 +87,8 @@
|
|
|
87
87
|
--plinth-ref-size-44: 2.75rem;
|
|
88
88
|
--plinth-ref-size-56: 3.5rem;
|
|
89
89
|
--plinth-ref-size-160: 10rem;
|
|
90
|
+
--plinth-ref-size-192: 12rem;
|
|
91
|
+
--plinth-ref-size-320: 20rem;
|
|
90
92
|
--plinth-ref-size-400: 25rem;
|
|
91
93
|
--plinth-ref-size-480: 30rem;
|
|
92
94
|
--plinth-ref-radius-0: 0;
|
|
@@ -199,6 +201,8 @@
|
|
|
199
201
|
--plinth-size-dialog: var(--plinth-ref-size-480);
|
|
200
202
|
--plinth-size-panel: var(--plinth-ref-size-400);
|
|
201
203
|
--plinth-size-peek: var(--plinth-ref-size-160);
|
|
204
|
+
--plinth-size-menu-min: var(--plinth-ref-size-192);
|
|
205
|
+
--plinth-size-menu-max: var(--plinth-ref-size-320);
|
|
202
206
|
--plinth-layer-base: 0;
|
|
203
207
|
--plinth-layer-sticky: 100;
|
|
204
208
|
--plinth-layer-overlay: 200;
|
|
@@ -206,7 +210,8 @@
|
|
|
206
210
|
--plinth-layer-toast: 400;
|
|
207
211
|
--plinth-breakpoint-md: 768px;
|
|
208
212
|
--plinth-breakpoint-lg: 1280px;
|
|
209
|
-
--plinth-header-height: var(--plinth-space-14);
|
|
213
|
+
--plinth-page-header-height: var(--plinth-space-14);
|
|
214
|
+
--plinth-page-header-offset: var(--plinth-space-0);
|
|
210
215
|
--plinth-data-table-row-height: var(--plinth-size-row);
|
|
211
216
|
--plinth-list-row-height: var(--plinth-size-row);
|
|
212
217
|
--plinth-pill-height: var(--plinth-control-md);
|
|
@@ -215,6 +220,9 @@
|
|
|
215
220
|
--plinth-drawer-peek-height: var(--plinth-size-peek);
|
|
216
221
|
--plinth-section-menu-height: var(--plinth-space-14);
|
|
217
222
|
--plinth-section-menu-item-height: var(--plinth-control-lg);
|
|
223
|
+
--plinth-menu-min-width: var(--plinth-size-menu-min);
|
|
224
|
+
--plinth-menu-max-width: var(--plinth-size-menu-max);
|
|
225
|
+
--plinth-text-field-trailing-size: var(--plinth-control-md);
|
|
218
226
|
}
|
|
219
227
|
|
|
220
228
|
:root,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
interface MenuOptions {
|
|
3
|
+
/** The menu's id, which its button names in `popoverTarget`. */
|
|
4
|
+
id: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A `<div popover="auto">`: open it with a button that has `popoverTarget` set to its id (or
|
|
8
|
+
* `commandfor` and `command="toggle-popover"`). Its `onToggle` is the popover's toggle event.
|
|
9
|
+
*/
|
|
10
|
+
export type PlinthMenuProps = MenuOptions & Omit<ComponentProps<'div'>, keyof MenuOptions>;
|
|
11
|
+
export declare function PlinthMenu({ id, className, children, ...rest }: PlinthMenuProps): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
interface MenuGroupOptions {
|
|
3
|
+
/** The heading over the group's items, and the group's accessible name. */
|
|
4
|
+
label?: string;
|
|
5
|
+
}
|
|
6
|
+
/** A `<div role="group">` of menu items under a heading. */
|
|
7
|
+
export type PlinthMenuGroupProps = MenuGroupOptions & Omit<ComponentProps<'div'>, keyof MenuGroupOptions>;
|
|
8
|
+
export declare function PlinthMenuGroup({ label, className, children, ...rest }: PlinthMenuGroupProps): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import type { InvokerCommandProps } from './button';
|
|
3
|
+
import { type MenuItemTone } from './menu';
|
|
4
|
+
interface MenuItemOptions {
|
|
5
|
+
/** `danger` for an item that destroys or removes something. */
|
|
6
|
+
tone?: MenuItemTone;
|
|
7
|
+
/** A keyboard shortcut to show at the item's end, such as ⌘D. */
|
|
8
|
+
shortcut?: string;
|
|
9
|
+
/** Disables the item. */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** The id of the menu a button item closes; the menu around it when missing. */
|
|
12
|
+
menu?: string;
|
|
13
|
+
/** The project's icon before the label, decorative (Vue's `icon` slot). */
|
|
14
|
+
icon?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A `<button>` that closes its menu when pressed (its own `commandfor` and `command` win), or
|
|
18
|
+
* with an `href` a link. The label is `children`.
|
|
19
|
+
*/
|
|
20
|
+
export type PlinthMenuItemProps = (MenuItemOptions & InvokerCommandProps & Omit<ComponentProps<'button'>, keyof MenuItemOptions | keyof InvokerCommandProps> & {
|
|
21
|
+
href?: undefined;
|
|
22
|
+
}) | (MenuItemOptions & Omit<ComponentProps<'a'>, keyof MenuItemOptions> & {
|
|
23
|
+
href: string;
|
|
24
|
+
});
|
|
25
|
+
export declare function PlinthMenuItem({ href, tone, shortcut, disabled, menu, icon, className, children, ...rest }: PlinthMenuItemProps): import("react").JSX.Element;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
/** An `<hr>` between a menu's items. */
|
|
3
|
+
export type PlinthMenuSeparatorProps = ComponentProps<'hr'>;
|
|
4
|
+
export declare function PlinthMenuSeparator({ className, ...rest }: PlinthMenuSeparatorProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
import { type PageHeaderLevel } from './page-header';
|
|
3
|
+
interface PageHeaderOptions {
|
|
4
|
+
/** The page's heading. */
|
|
5
|
+
heading: string;
|
|
6
|
+
/** The heading's element: `h1`, or `h2` to `h6` beside another page's `h1`. */
|
|
7
|
+
level?: PageHeaderLevel;
|
|
8
|
+
/** The screen's top bar: it adds the top safe-area inset (data-top). */
|
|
9
|
+
top?: boolean;
|
|
10
|
+
/** The leading action, usually a menu button or a back link (Vue's `leading` slot). */
|
|
11
|
+
leading?: ReactNode;
|
|
12
|
+
/** The trailing actions: icon buttons or a primary button (Vue's `actions` slot). */
|
|
13
|
+
actions?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export type PlinthPageHeaderProps = PageHeaderOptions & Omit<ComponentProps<'header'>, keyof PageHeaderOptions | 'children'>;
|
|
16
|
+
export declare function PlinthPageHeader({ heading, level, top, leading, actions, className, ...rest }: PlinthPageHeaderProps): import("react").JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import { type TextFieldSize } from './text-field';
|
|
3
|
+
interface TextFieldOptions {
|
|
4
|
+
/** The label, and the field's accessible name. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Hides the label visually; screen readers still read it. */
|
|
7
|
+
hideLabel?: boolean;
|
|
8
|
+
size?: TextFieldSize;
|
|
9
|
+
/** A line under the field that helps fill it in. */
|
|
10
|
+
description?: string;
|
|
11
|
+
/** What is wrong with the value; it marks the field invalid. */
|
|
12
|
+
error?: string;
|
|
13
|
+
/** The leading icon (Vue's `icon` slot). */
|
|
14
|
+
icon?: ReactNode;
|
|
15
|
+
/** A small control or hint at the end, such as a clear button (Vue's `trailing` slot). */
|
|
16
|
+
trailing?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A text field. `className` goes on the root; every other prop (`value`, `onChange`, `name`,
|
|
20
|
+
* `placeholder`, `ref`, …) on the `<input>`.
|
|
21
|
+
*/
|
|
22
|
+
export type PlinthTextFieldProps = TextFieldOptions & Omit<ComponentProps<'input'>, keyof TextFieldOptions | 'children'>;
|
|
23
|
+
export declare function PlinthTextField({ label, hideLabel, id, type, size, description, error, icon, trailing, className, ...rest }: PlinthTextFieldProps): import("react").JSX.Element;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ComponentProps } from 'react';
|
|
2
|
+
interface TextareaOptions {
|
|
3
|
+
/** The label, and the field's accessible name. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Hides the label visually; screen readers still read it. */
|
|
6
|
+
hideLabel?: boolean;
|
|
7
|
+
/** The lines it shows, and where it starts when it grows. */
|
|
8
|
+
rows?: number;
|
|
9
|
+
/** Grows with its content, from rows lines to maxRows, then scrolls. */
|
|
10
|
+
grow?: boolean;
|
|
11
|
+
/** The most lines it grows to. */
|
|
12
|
+
maxRows?: number;
|
|
13
|
+
/** A line under the field that helps fill it in. */
|
|
14
|
+
description?: string;
|
|
15
|
+
/** What is wrong with the value; it marks the field invalid. */
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A textarea. `className` goes on the root; every other prop (`value`, `onChange`, `name`,
|
|
20
|
+
* `placeholder`, `ref`, …) on the `<textarea>`.
|
|
21
|
+
*/
|
|
22
|
+
export type PlinthTextareaProps = TextareaOptions & Omit<ComponentProps<'textarea'>, keyof TextareaOptions | 'children'>;
|
|
23
|
+
export declare function PlinthTextarea({ label, hideLabel, id, rows, grow, maxRows, description, error, className, style, ...rest }: PlinthTextareaProps): import("react").JSX.Element;
|
|
24
|
+
export {};
|
|
@@ -12,17 +12,23 @@ export { PlinthDataTableRow, type PlinthDataTableRowProps } from './PlinthDataTa
|
|
|
12
12
|
export { PlinthDataTableSort, type PlinthDataTableSortProps } from './PlinthDataTableSort';
|
|
13
13
|
export { PlinthDialog, type PlinthDialogProps } from './PlinthDialog';
|
|
14
14
|
export { PlinthDrawer, type PlinthDrawerProps } from './PlinthDrawer';
|
|
15
|
-
export { PlinthHeader, type PlinthHeaderProps } from './PlinthHeader';
|
|
16
15
|
export { PlinthIcon, type PlinthIconProps } from './PlinthIcon';
|
|
17
16
|
export { PlinthIconButton, type PlinthIconButtonProps } from './PlinthIconButton';
|
|
18
17
|
export { PlinthListCard, type PlinthListCardProps } from './PlinthListCard';
|
|
19
18
|
export { PlinthListRow, type PlinthListRowProps } from './PlinthListRow';
|
|
19
|
+
export { PlinthMenu, type PlinthMenuProps } from './PlinthMenu';
|
|
20
|
+
export { PlinthMenuGroup, type PlinthMenuGroupProps } from './PlinthMenuGroup';
|
|
21
|
+
export { PlinthMenuItem, type PlinthMenuItemProps } from './PlinthMenuItem';
|
|
22
|
+
export { PlinthMenuSeparator, type PlinthMenuSeparatorProps } from './PlinthMenuSeparator';
|
|
20
23
|
export { PlinthMenuSheet, type PlinthMenuSheetProps } from './PlinthMenuSheet';
|
|
24
|
+
export { PlinthPageHeader, type PlinthPageHeaderProps } from './PlinthPageHeader';
|
|
21
25
|
export { PlinthPill, type PlinthPillProps } from './PlinthPill';
|
|
22
26
|
export { PlinthPillRow, type PlinthPillRowProps } from './PlinthPillRow';
|
|
23
27
|
export { PlinthSectionMenu, type PlinthSectionMenuProps } from './PlinthSectionMenu';
|
|
24
28
|
export { PlinthSectionMenuItem, type PlinthSectionMenuItemProps } from './PlinthSectionMenuItem';
|
|
25
29
|
export { PlinthSheet, type PlinthSheetProps } from './PlinthSheet';
|
|
30
|
+
export { PlinthTextarea, type PlinthTextareaProps } from './PlinthTextarea';
|
|
31
|
+
export { PlinthTextField, type PlinthTextFieldProps } from './PlinthTextField';
|
|
26
32
|
export { avatarGlyph, avatarSizes, initialsFrom, type AvatarSize } from './avatar';
|
|
27
33
|
export { buttonVariants, type ButtonVariant, type InvokerCommand, type InvokerCommandProps, } from './button';
|
|
28
34
|
export { calloutIcons, calloutRole, calloutRoles, calloutTone, calloutTones, type CalloutRole, type CalloutTone, } from './callout';
|
|
@@ -34,7 +40,10 @@ export { headingLevel, headingLevels, type HeadingLevel } from './heading';
|
|
|
34
40
|
export { controlSizes, iconSizeFor, resolveSize, sizes, type ControlSize, type Size } from './size';
|
|
35
41
|
export { iconSizes, type IconSize } from './icon';
|
|
36
42
|
export { listRowChevron } from './list-row';
|
|
43
|
+
export { MenuIdContext, menuItemTone, menuItemTones, type MenuItemTone } from './menu';
|
|
44
|
+
export { pageHeaderLevel, pageHeaderLevels, type PageHeaderLevel } from './page-header';
|
|
37
45
|
export { drawerCloseIcon, drawerDesktop, drawerDesktops, drawerSnap, drawerSnapPoints, drawerSnaps, type DrawerDesktop, type DrawerSnap, } from './drawer';
|
|
38
46
|
export { swipeToClose, useSwipeToClose } from './swipe';
|
|
47
|
+
export { fieldDescribedBy, textFieldSize, textFieldSizes, type TextFieldSize } from './text-field';
|
|
39
48
|
export { sheetCloseIcon, sheetSide, sheetSidePhone, sheetSides, type SheetSide } from './sheet';
|
|
40
49
|
export { tones, type Tone } from './tones';
|