@pienter/ui 0.10.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.
- package/CHANGELOG.md +91 -0
- package/CONVENTIONS.md +33 -21
- package/components/display/badge/badge.css +7 -0
- package/components/form/block-editor/BlockEditor.vue +61 -44
- package/components/form/block-editor/block-editor.css +11 -0
- package/components/form/checkbox/Checkbox.vue +23 -7
- package/components/form/checkbox/checkbox.css +18 -0
- package/components/form/combobox/Combobox.vue +18 -3
- package/components/form/date-input/DateInput.vue +13 -1
- package/components/form/form/Form.vue +5 -2
- package/components/form/number-field/NumberField.vue +16 -4
- package/components/form/number-field/number-field.css +6 -0
- package/components/form/radio-group/RadioGroup.vue +23 -1
- package/components/form/radio-group/radio-group.css +9 -0
- package/components/form/record-form/RecordFields.vue +40 -2
- package/components/form/record-form/RecordForm.vue +7 -1
- package/components/form/record-form/record-form.css +14 -0
- package/components/form/record-form/types.ts +6 -0
- package/components/form/select/Select.vue +40 -7
- package/components/form/select/select.css +5 -0
- package/components/form/switch/Switch.vue +23 -7
- package/components/form/switch/switch.css +14 -0
- package/components/form/tags-input/TagsInput.vue +19 -6
- package/components/form/tags-input/tags-input.css +5 -0
- package/components/form/text-input/TextInput.vue +14 -2
- package/components/form/textarea/Textarea.vue +14 -2
- package/components/form/textarea/textarea.css +7 -0
- package/components/layout/index/Index.vue +1 -0
- package/components/layout/index/index.css +8 -7
- package/components/layout/record-layout/Panel.vue +44 -0
- package/components/layout/record-layout/RecordLayout.vue +35 -0
- package/components/layout/record-layout/record-layout.css +94 -0
- package/components/navigation/tabs/tabs.css +2 -0
- package/package.json +3 -1
- package/styles/2-base/base.css +6 -1
- package/styles/4-components/form-field.css +10 -7
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="pui-field"
|
|
4
|
+
:data-layout="layout === 'inline' ? 'inline' : undefined"
|
|
5
|
+
:data-readonly="readonly ? 'true' : undefined"
|
|
6
|
+
:data-status="computedStatus"
|
|
7
|
+
>
|
|
3
8
|
<label class="pui-field__label" :for="inputId"
|
|
4
9
|
>{{ label
|
|
5
10
|
}}<span
|
|
@@ -31,6 +36,7 @@
|
|
|
31
36
|
>
|
|
32
37
|
<span class="pui-tags-input__tag-label">{{ tag }}</span>
|
|
33
38
|
<button
|
|
39
|
+
v-if="!readonly"
|
|
34
40
|
type="button"
|
|
35
41
|
class="pui-tags-input__tag-remove"
|
|
36
42
|
:aria-label="`Remove tag ${tag}`"
|
|
@@ -48,8 +54,9 @@
|
|
|
48
54
|
class="pui-tags-input__entry"
|
|
49
55
|
type="text"
|
|
50
56
|
:name="name"
|
|
51
|
-
:placeholder="placeholder"
|
|
52
|
-
:disabled="disabled || maxReached"
|
|
57
|
+
:placeholder="readonly ? undefined : placeholder"
|
|
58
|
+
:disabled="disabled || (!readonly && maxReached)"
|
|
59
|
+
:readonly="readonly"
|
|
53
60
|
:aria-required="required ? 'true' : undefined"
|
|
54
61
|
:aria-invalid="hasErrors ? 'true' : undefined"
|
|
55
62
|
:aria-describedby="describedBy"
|
|
@@ -87,6 +94,8 @@ const props = withDefaults(
|
|
|
87
94
|
/** Marks the field; set as `aria-required` on the entry input since the value is the tag list, not the typed text. */
|
|
88
95
|
required?: boolean;
|
|
89
96
|
disabled?: boolean;
|
|
97
|
+
/** Read-only: tags show without remove buttons and the entry is a focusable native `readonly` input. */
|
|
98
|
+
readonly?: boolean;
|
|
90
99
|
hint?: string;
|
|
91
100
|
errors?: string[];
|
|
92
101
|
status?: 'error' | 'success';
|
|
@@ -100,6 +109,8 @@ const props = withDefaults(
|
|
|
100
109
|
* `[',', 'Enter']`.
|
|
101
110
|
*/
|
|
102
111
|
separators?: string[];
|
|
112
|
+
/** `inline` puts the label beside the control; hint and errors wrap below. */
|
|
113
|
+
layout?: 'stacked' | 'inline';
|
|
103
114
|
}>(),
|
|
104
115
|
{
|
|
105
116
|
id: undefined,
|
|
@@ -107,11 +118,13 @@ const props = withDefaults(
|
|
|
107
118
|
placeholder: undefined,
|
|
108
119
|
required: false,
|
|
109
120
|
disabled: false,
|
|
121
|
+
readonly: false,
|
|
110
122
|
hint: undefined,
|
|
111
123
|
errors: () => [],
|
|
112
124
|
status: undefined,
|
|
113
125
|
maxTags: undefined,
|
|
114
126
|
separators: () => [',', 'Enter'],
|
|
127
|
+
layout: 'stacked',
|
|
115
128
|
},
|
|
116
129
|
);
|
|
117
130
|
|
|
@@ -156,7 +169,7 @@ function isSeparator(event: KeyboardEvent): boolean {
|
|
|
156
169
|
|
|
157
170
|
function addTag(raw: string): boolean {
|
|
158
171
|
const value = raw.trim();
|
|
159
|
-
if (!value) return false;
|
|
172
|
+
if (!value || props.readonly) return false;
|
|
160
173
|
if (maxReached.value) return false;
|
|
161
174
|
if (props.modelValue.includes(value)) return false;
|
|
162
175
|
emit('update:modelValue', [...props.modelValue, value]);
|
|
@@ -164,7 +177,7 @@ function addTag(raw: string): boolean {
|
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
function removeTag(index: number): void {
|
|
167
|
-
if (props.disabled) return;
|
|
180
|
+
if (props.disabled || props.readonly) return;
|
|
168
181
|
if (index < 0 || index >= props.modelValue.length) return;
|
|
169
182
|
const tag = props.modelValue[index];
|
|
170
183
|
const next = props.modelValue.filter((_, i) => i !== index);
|
|
@@ -175,7 +188,7 @@ function removeTag(index: number): void {
|
|
|
175
188
|
}
|
|
176
189
|
|
|
177
190
|
function onKeydown(event: KeyboardEvent): void {
|
|
178
|
-
if (props.disabled) return;
|
|
191
|
+
if (props.disabled || props.readonly) return;
|
|
179
192
|
if (isSeparator(event)) {
|
|
180
193
|
if (entryText.value.trim().length > 0) {
|
|
181
194
|
event.preventDefault();
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
cursor: not-allowed;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
.pui-field[data-readonly='true'] .pui-tags-input {
|
|
51
|
+
background: var(--bg-clr-surface-2);
|
|
52
|
+
cursor: default;
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
/* Status states inherited from .pui-field — recolour to match
|
|
51
56
|
* .pui-input / .pui-textarea when the parent carries `data-status`. */
|
|
52
57
|
.pui-field[data-status='error'] .pui-tags-input {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="pui-field"
|
|
4
|
+
:data-layout="layout === 'inline' ? 'inline' : undefined"
|
|
5
|
+
:data-readonly="readonly ? 'true' : undefined"
|
|
6
|
+
:data-status="computedStatus"
|
|
7
|
+
>
|
|
3
8
|
<label class="pui-field__label" :for="inputId"
|
|
4
9
|
>{{ label
|
|
5
10
|
}}<span
|
|
@@ -19,7 +24,8 @@
|
|
|
19
24
|
:autocomplete="autocomplete"
|
|
20
25
|
:required="required"
|
|
21
26
|
:disabled="disabled"
|
|
22
|
-
:
|
|
27
|
+
:readonly="readonly"
|
|
28
|
+
:placeholder="readonly ? undefined : placeholder"
|
|
23
29
|
:aria-invalid="hasErrors ? 'true' : undefined"
|
|
24
30
|
:aria-describedby="describedBy"
|
|
25
31
|
@input="
|
|
@@ -58,10 +64,14 @@ const props = withDefaults(
|
|
|
58
64
|
autocomplete?: string;
|
|
59
65
|
required?: boolean;
|
|
60
66
|
disabled?: boolean;
|
|
67
|
+
/** Read-only: the value shows in the field chrome, stays focusable and cannot be edited. */
|
|
68
|
+
readonly?: boolean;
|
|
61
69
|
placeholder?: string;
|
|
62
70
|
hint?: string;
|
|
63
71
|
errors?: string[];
|
|
64
72
|
status?: 'error' | 'success';
|
|
73
|
+
/** `inline` puts the label beside the control; hint and errors wrap below. */
|
|
74
|
+
layout?: 'stacked' | 'inline';
|
|
65
75
|
}>(),
|
|
66
76
|
{
|
|
67
77
|
id: undefined,
|
|
@@ -70,10 +80,12 @@ const props = withDefaults(
|
|
|
70
80
|
autocomplete: undefined,
|
|
71
81
|
required: false,
|
|
72
82
|
disabled: false,
|
|
83
|
+
readonly: false,
|
|
73
84
|
placeholder: undefined,
|
|
74
85
|
hint: undefined,
|
|
75
86
|
errors: () => [],
|
|
76
87
|
status: undefined,
|
|
88
|
+
layout: 'stacked',
|
|
77
89
|
},
|
|
78
90
|
);
|
|
79
91
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="pui-field"
|
|
4
|
+
:data-layout="layout === 'inline' ? 'inline' : undefined"
|
|
5
|
+
:data-readonly="readonly ? 'true' : undefined"
|
|
6
|
+
:data-status="computedStatus"
|
|
7
|
+
>
|
|
3
8
|
<label class="pui-field__label" :for="inputId"
|
|
4
9
|
>{{ label
|
|
5
10
|
}}<span
|
|
@@ -18,7 +23,8 @@
|
|
|
18
23
|
:autocomplete="autocomplete"
|
|
19
24
|
:required="required"
|
|
20
25
|
:disabled="disabled"
|
|
21
|
-
:
|
|
26
|
+
:readonly="readonly"
|
|
27
|
+
:placeholder="readonly ? undefined : placeholder"
|
|
22
28
|
:style="autoResize ? 'field-sizing: content' : undefined"
|
|
23
29
|
:aria-invalid="hasErrors ? 'true' : undefined"
|
|
24
30
|
:aria-describedby="describedBy"
|
|
@@ -59,11 +65,15 @@ const props = withDefaults(
|
|
|
59
65
|
autocomplete?: string;
|
|
60
66
|
required?: boolean;
|
|
61
67
|
disabled?: boolean;
|
|
68
|
+
/** Read-only: the value shows in the field chrome, stays focusable and cannot be edited. */
|
|
69
|
+
readonly?: boolean;
|
|
62
70
|
placeholder?: string;
|
|
63
71
|
hint?: string;
|
|
64
72
|
errors?: string[];
|
|
65
73
|
status?: 'error' | 'success';
|
|
66
74
|
autoResize?: boolean;
|
|
75
|
+
/** `inline` puts the label beside the control; hint and errors wrap below. */
|
|
76
|
+
layout?: 'stacked' | 'inline';
|
|
67
77
|
}>(),
|
|
68
78
|
{
|
|
69
79
|
id: undefined,
|
|
@@ -72,11 +82,13 @@ const props = withDefaults(
|
|
|
72
82
|
autocomplete: undefined,
|
|
73
83
|
required: false,
|
|
74
84
|
disabled: false,
|
|
85
|
+
readonly: false,
|
|
75
86
|
placeholder: undefined,
|
|
76
87
|
hint: undefined,
|
|
77
88
|
errors: () => [],
|
|
78
89
|
status: undefined,
|
|
79
90
|
autoResize: false,
|
|
91
|
+
layout: 'stacked',
|
|
80
92
|
},
|
|
81
93
|
);
|
|
82
94
|
|
|
@@ -6,4 +6,11 @@
|
|
|
6
6
|
.pui-textarea[style*='field-sizing'] {
|
|
7
7
|
resize: none;
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
/* Read-only: `rows` becomes a minimum and the box grows, so a long value
|
|
11
|
+
* is read in place rather than scrolled inside a box nobody can type in. */
|
|
12
|
+
.pui-field[data-readonly='true'] .pui-textarea {
|
|
13
|
+
field-sizing: content;
|
|
14
|
+
resize: none;
|
|
15
|
+
}
|
|
9
16
|
}
|
|
@@ -47,14 +47,15 @@
|
|
|
47
47
|
--pui-table-cell-padding-block: var(--space-2xs);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
gap: var(--space-
|
|
50
|
+
/* Two-line primary cell: title (usually a link) over a muted subline. */
|
|
51
|
+
.pui-index__primary {
|
|
52
|
+
display: grid;
|
|
53
|
+
gap: var(--space-3xs);
|
|
54
|
+
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
.pui-index__subline {
|
|
57
|
+
color: var(--text-clr-muted);
|
|
58
|
+
font-size: var(--step--2);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
.pui-index__pagination {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="pui-panel">
|
|
3
|
+
<header v-if="title || description" class="pui-panel__header">
|
|
4
|
+
<component
|
|
5
|
+
:is="`h${level}`"
|
|
6
|
+
v-if="title"
|
|
7
|
+
:id="titleId"
|
|
8
|
+
class="pui-panel__title"
|
|
9
|
+
>{{ title }}</component
|
|
10
|
+
>
|
|
11
|
+
<p v-if="description" class="pui-panel__description">
|
|
12
|
+
{{ description }}
|
|
13
|
+
</p>
|
|
14
|
+
</header>
|
|
15
|
+
<slot />
|
|
16
|
+
</section>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
withDefaults(
|
|
21
|
+
defineProps<{
|
|
22
|
+
title?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
/** Heading level of the title: `2` under a page heading, `3` under another section. */
|
|
25
|
+
level?: 2 | 3;
|
|
26
|
+
/** `id` for the title element, so the panel or a landmark can reference it through `aria-labelledby`. */
|
|
27
|
+
titleId?: string;
|
|
28
|
+
}>(),
|
|
29
|
+
{
|
|
30
|
+
title: undefined,
|
|
31
|
+
description: undefined,
|
|
32
|
+
level: 2,
|
|
33
|
+
titleId: undefined,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
defineSlots<{
|
|
38
|
+
default?: () => unknown;
|
|
39
|
+
}>();
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
@import './record-layout.css';
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="pui-record-layout">
|
|
3
|
+
<aside
|
|
4
|
+
v-if="$slots.sidebar"
|
|
5
|
+
class="pui-record-layout__sidebar"
|
|
6
|
+
:aria-labelledby="sidebarLabelledby"
|
|
7
|
+
>
|
|
8
|
+
<slot name="sidebar" />
|
|
9
|
+
</aside>
|
|
10
|
+
<div class="pui-record-layout__main">
|
|
11
|
+
<slot />
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
/** `id` of the sidebar's heading; names the `<aside>` landmark through `aria-labelledby`. */
|
|
20
|
+
sidebarLabelledby?: string;
|
|
21
|
+
}>(),
|
|
22
|
+
{
|
|
23
|
+
sidebarLabelledby: undefined,
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
defineSlots<{
|
|
28
|
+
sidebar?: () => unknown;
|
|
29
|
+
default?: () => unknown;
|
|
30
|
+
}>();
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
@import './record-layout.css';
|
|
35
|
+
</style>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pui-record-layout {
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-template-columns:
|
|
5
|
+
minmax(0, var(--space-sidebar-width))
|
|
6
|
+
minmax(0, 1fr);
|
|
7
|
+
gap: var(--space-m);
|
|
8
|
+
align-items: start;
|
|
9
|
+
min-inline-size: 0;
|
|
10
|
+
|
|
11
|
+
@media (max-width: 56em) {
|
|
12
|
+
grid-template-columns: minmax(0, 1fr);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.pui-record-layout__sidebar {
|
|
17
|
+
min-inline-size: 0;
|
|
18
|
+
|
|
19
|
+
& .pui-panel {
|
|
20
|
+
--pui-record-fields-columns: minmax(0, 1fr);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Same layer as record-details.css, so this has to outrank its field rule. */
|
|
24
|
+
& .pui-record-details .pui-record-details__field {
|
|
25
|
+
grid-template-columns: minmax(0, 1fr);
|
|
26
|
+
gap: var(--space-3xs);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@media (min-width: 24em) and (max-width: 56em) {
|
|
30
|
+
& .pui-panel {
|
|
31
|
+
--pui-record-fields-columns: repeat(2, minmax(0, 1fr));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.pui-record-layout__main {
|
|
37
|
+
display: grid;
|
|
38
|
+
gap: var(--space-s);
|
|
39
|
+
min-inline-size: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.pui-panel {
|
|
43
|
+
--pui-record-fields-columns: minmax(0, 1fr);
|
|
44
|
+
|
|
45
|
+
display: grid;
|
|
46
|
+
gap: var(--space-s);
|
|
47
|
+
min-inline-size: 0;
|
|
48
|
+
padding: var(--space-m);
|
|
49
|
+
border: var(--stroke-sm) solid var(--border-clr-subtle);
|
|
50
|
+
border-radius: var(--radius-md);
|
|
51
|
+
background: var(--bg-clr-surface);
|
|
52
|
+
|
|
53
|
+
&[hidden] {
|
|
54
|
+
display: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
& .pui-field:has(textarea) {
|
|
58
|
+
grid-column: 1 / -1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* An editable block list draws its own boxes and needs the width for
|
|
62
|
+
* its controls; a read-only one is content and keeps the panel. */
|
|
63
|
+
&:has(> .pui-block-editor:not([data-readonly='true'])) {
|
|
64
|
+
padding: var(--space-2xs) 0 0;
|
|
65
|
+
border: 0;
|
|
66
|
+
background: transparent;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media (min-width: 48em) {
|
|
70
|
+
--pui-record-fields-columns: repeat(2, minmax(0, 1fr));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@media (max-width: 56em) {
|
|
74
|
+
padding: var(--space-s);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.pui-panel__header {
|
|
79
|
+
display: grid;
|
|
80
|
+
gap: var(--space-3xs);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.pui-panel__title {
|
|
84
|
+
margin: 0;
|
|
85
|
+
font-size: var(--step-0);
|
|
86
|
+
font-weight: var(--fw-semibold);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.pui-panel__description {
|
|
90
|
+
margin: 0;
|
|
91
|
+
font-size: var(--step--1);
|
|
92
|
+
color: var(--text-clr-muted);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pienter/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Shared Pienter UI components, styles, icons, and browser utilities.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"./components/Collapsible.vue": "./components/layout/collapsible/Collapsible.vue",
|
|
65
65
|
"./components/AppLayout.vue": "./components/layout/app-layout/AppLayout.vue",
|
|
66
66
|
"./components/PageHeader.vue": "./components/layout/page-header/PageHeader.vue",
|
|
67
|
+
"./components/Panel.vue": "./components/layout/record-layout/Panel.vue",
|
|
68
|
+
"./components/RecordLayout.vue": "./components/layout/record-layout/RecordLayout.vue",
|
|
67
69
|
"./components/Separator.vue": "./components/layout/separator/Separator.vue",
|
|
68
70
|
"./components/Table.vue": "./components/layout/table/Table.vue",
|
|
69
71
|
"./components/TableRow.vue": "./components/layout/table/TableRow.vue",
|
package/styles/2-base/base.css
CHANGED
|
@@ -33,10 +33,15 @@
|
|
|
33
33
|
|
|
34
34
|
a {
|
|
35
35
|
color: var(--text-clr-brand);
|
|
36
|
-
text-underline-offset:
|
|
36
|
+
text-underline-offset: var(--space-3xs);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
a:hover {
|
|
40
40
|
color: var(--text-clr-brand-hover);
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
a:focus-visible {
|
|
44
|
+
outline: var(--outline-width) solid var(--outline-clr-base);
|
|
45
|
+
outline-offset: var(--outline-offset);
|
|
46
|
+
}
|
|
42
47
|
}
|
|
@@ -27,19 +27,15 @@
|
|
|
27
27
|
float: none;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/* `data-layout="inline"`: control
|
|
31
|
-
*
|
|
30
|
+
/* `data-layout="inline"`: label and control share a row in DOM order
|
|
31
|
+
* (control first for Checkbox/Switch, label first elsewhere); hint and
|
|
32
|
+
* errors drop below spanning both columns. */
|
|
32
33
|
.pui-field[data-layout='inline'] {
|
|
33
34
|
grid-template-columns: max-content 1fr;
|
|
34
35
|
column-gap: var(--space-xs);
|
|
35
36
|
align-items: center;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
.pui-field[data-layout='inline'] .pui-field__label {
|
|
39
|
-
cursor: pointer;
|
|
40
|
-
user-select: none;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
39
|
.pui-field[data-layout='inline'] .pui-field__hint {
|
|
44
40
|
grid-column: 1 / -1;
|
|
45
41
|
}
|
|
@@ -100,6 +96,13 @@
|
|
|
100
96
|
cursor: not-allowed;
|
|
101
97
|
}
|
|
102
98
|
|
|
99
|
+
/* `data-readonly="true"`: same border, text and focus ring as the
|
|
100
|
+
* default state on a muted surface; the control stays in the tab order. */
|
|
101
|
+
.pui-field[data-readonly='true'] .pui-input,
|
|
102
|
+
.pui-field[data-readonly='true'] .pui-textarea {
|
|
103
|
+
background: var(--bg-clr-surface-2);
|
|
104
|
+
}
|
|
105
|
+
|
|
103
106
|
.pui-field[data-status='error'] .pui-input,
|
|
104
107
|
.pui-field[data-status='error'] .pui-textarea {
|
|
105
108
|
border-color: var(--border-clr-danger);
|