qdadm 0.35.0 → 0.38.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 +27 -174
- package/package.json +2 -1
- package/src/auth/SessionAuthAdapter.js +114 -3
- package/src/components/editors/PermissionEditor.vue +535 -0
- package/src/components/forms/FormField.vue +1 -11
- package/src/components/index.js +1 -0
- package/src/components/layout/AppLayout.vue +20 -8
- package/src/components/layout/defaults/DefaultToaster.vue +3 -3
- package/src/components/pages/LoginPage.vue +26 -5
- package/src/composables/useCurrentEntity.js +26 -17
- package/src/composables/useForm.js +7 -0
- package/src/composables/useFormPageBuilder.js +7 -0
- package/src/composables/useNavContext.js +30 -16
- package/src/core/index.js +0 -3
- package/src/debug/AuthCollector.js +199 -31
- package/src/debug/Collector.js +24 -2
- package/src/debug/EntitiesCollector.js +8 -0
- package/src/debug/SignalCollector.js +60 -2
- package/src/debug/components/panels/AuthPanel.vue +159 -27
- package/src/debug/components/panels/EntitiesPanel.vue +18 -2
- package/src/entity/EntityManager.js +205 -36
- package/src/entity/auth/EntityAuthAdapter.js +54 -46
- package/src/entity/auth/SecurityChecker.js +110 -42
- package/src/entity/auth/factory.js +11 -2
- package/src/entity/auth/factory.test.js +29 -0
- package/src/entity/storage/factory.test.js +6 -5
- package/src/index.js +3 -0
- package/src/kernel/Kernel.js +135 -25
- package/src/kernel/KernelContext.js +166 -0
- package/src/security/EntityRoleGranterAdapter.js +350 -0
- package/src/security/PermissionMatcher.js +148 -0
- package/src/security/PermissionRegistry.js +263 -0
- package/src/security/PersistableRoleGranterAdapter.js +618 -0
- package/src/security/RoleGranterAdapter.js +123 -0
- package/src/security/RoleGranterStorage.js +161 -0
- package/src/security/RolesManager.js +81 -0
- package/src/security/SecurityModule.js +73 -0
- package/src/security/StaticRoleGranterAdapter.js +114 -0
- package/src/security/UsersManager.js +122 -0
- package/src/security/index.js +45 -0
- package/src/security/pages/RoleForm.vue +212 -0
- package/src/security/pages/RoleList.vue +106 -0
- package/src/styles/main.css +62 -2
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security module - Permission and role management
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - PermissionMatcher: Wildcard pattern matching (*, **)
|
|
6
|
+
* - PermissionRegistry: Central registry for module permissions
|
|
7
|
+
* - RoleGranterAdapter: Interface for role → permissions mapping
|
|
8
|
+
* - StaticRoleGranterAdapter: Config-based role granter (default)
|
|
9
|
+
* - EntityRoleGranterAdapter: Entity-based role granter (for UI management)
|
|
10
|
+
* - PersistableRoleGranterAdapter: Load/persist from any source (localStorage, API)
|
|
11
|
+
* - SecurityModule: System module for roles UI (uses RolesManager)
|
|
12
|
+
* - RolesManager: System entity manager for roles
|
|
13
|
+
* - UsersManager: System entity manager for users (linked to roles)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import {
|
|
17
|
+
* PermissionMatcher,
|
|
18
|
+
* PermissionRegistry,
|
|
19
|
+
* StaticRoleGranterAdapter,
|
|
20
|
+
* EntityRoleGranterAdapter,
|
|
21
|
+
* PersistableRoleGranterAdapter,
|
|
22
|
+
* createLocalStorageRoleGranter,
|
|
23
|
+
* UsersManager
|
|
24
|
+
* } from 'qdadm/security'
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export { PermissionMatcher } from './PermissionMatcher.js'
|
|
28
|
+
export { PermissionRegistry } from './PermissionRegistry.js'
|
|
29
|
+
export { RoleGranterAdapter } from './RoleGranterAdapter.js'
|
|
30
|
+
export { StaticRoleGranterAdapter } from './StaticRoleGranterAdapter.js'
|
|
31
|
+
export { EntityRoleGranterAdapter } from './EntityRoleGranterAdapter.js'
|
|
32
|
+
export {
|
|
33
|
+
PersistableRoleGranterAdapter,
|
|
34
|
+
createLocalStorageRoleGranter
|
|
35
|
+
} from './PersistableRoleGranterAdapter.js'
|
|
36
|
+
export { SecurityModule } from './SecurityModule.js'
|
|
37
|
+
export { RolesManager } from './RolesManager.js'
|
|
38
|
+
export { UsersManager } from './UsersManager.js'
|
|
39
|
+
export { RoleGranterStorage } from './RoleGranterStorage.js'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Standard entity actions for CRUD operations
|
|
43
|
+
* Used by PermissionRegistry.registerEntity()
|
|
44
|
+
*/
|
|
45
|
+
export const ENTITY_ACTIONS = ['read', 'list', 'create', 'update', 'delete']
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* RoleForm - Role create/edit form (standard FormPage pattern)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ref, computed, inject } from 'vue'
|
|
7
|
+
import { useFormPageBuilder, FormPage, useOrchestrator, PermissionEditor } from '../../index.js'
|
|
8
|
+
import InputText from 'primevue/inputtext'
|
|
9
|
+
import AutoComplete from 'primevue/autocomplete'
|
|
10
|
+
import Chip from 'primevue/chip'
|
|
11
|
+
|
|
12
|
+
// ============ FORM BUILDER ============
|
|
13
|
+
const form = useFormPageBuilder({ entity: 'roles' })
|
|
14
|
+
|
|
15
|
+
// ============ HELPERS ============
|
|
16
|
+
const { getManager } = useOrchestrator()
|
|
17
|
+
const manager = getManager('roles')
|
|
18
|
+
|
|
19
|
+
// Get permissionRegistry directly from Kernel (via provide/inject)
|
|
20
|
+
const permissionRegistry = inject('qdadmPermissionRegistry', null)
|
|
21
|
+
|
|
22
|
+
// Role options for inheritance (exclude self)
|
|
23
|
+
const allRoles = computed(() => {
|
|
24
|
+
const currentName = form.data.value?.name
|
|
25
|
+
const roles = manager?.roleGranter?.getRoles() || []
|
|
26
|
+
return roles
|
|
27
|
+
.filter(name => name !== currentName)
|
|
28
|
+
.map(name => {
|
|
29
|
+
const role = manager?.roleGranter?.getRole?.(name)
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
label: role?.label || name,
|
|
33
|
+
display: `${name} (${role?.label || name})`
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Autocomplete for roles
|
|
39
|
+
const roleInput = ref('')
|
|
40
|
+
const roleSuggestions = ref([])
|
|
41
|
+
|
|
42
|
+
function searchRoles(event) {
|
|
43
|
+
const query = (event.query || '').toLowerCase()
|
|
44
|
+
const selected = form.data.value.inherits || []
|
|
45
|
+
|
|
46
|
+
roleSuggestions.value = allRoles.value
|
|
47
|
+
.filter(r => !selected.includes(r.name))
|
|
48
|
+
.filter(r =>
|
|
49
|
+
query === '' ||
|
|
50
|
+
r.name.toLowerCase().includes(query) ||
|
|
51
|
+
r.label.toLowerCase().includes(query)
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function onRoleSelect(event) {
|
|
56
|
+
const role = event.value
|
|
57
|
+
if (role && !form.data.value.inherits?.includes(role.name)) {
|
|
58
|
+
form.data.value.inherits = [...(form.data.value.inherits || []), role.name]
|
|
59
|
+
}
|
|
60
|
+
roleInput.value = ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function removeRole(roleName) {
|
|
64
|
+
form.data.value.inherits = (form.data.value.inherits || []).filter(r => r !== roleName)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getRoleLabel(roleName) {
|
|
68
|
+
const role = allRoles.value.find(r => r.name === roleName)
|
|
69
|
+
return role?.label || roleName
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<template>
|
|
74
|
+
<FormPage v-bind="form.props.value" v-on="form.events">
|
|
75
|
+
<template #fields>
|
|
76
|
+
<div class="role-form-fields">
|
|
77
|
+
<!-- Role Name -->
|
|
78
|
+
<div class="form-field">
|
|
79
|
+
<label class="font-medium">Role Name</label>
|
|
80
|
+
<InputText
|
|
81
|
+
v-model="form.data.value.name"
|
|
82
|
+
:disabled="form.isEdit.value"
|
|
83
|
+
placeholder="ROLE_ADMIN"
|
|
84
|
+
class="w-full"
|
|
85
|
+
/>
|
|
86
|
+
<small class="field-hint">Convention: ROLE_UPPERCASE_NAME</small>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Label -->
|
|
90
|
+
<div class="form-field">
|
|
91
|
+
<label class="font-medium">Display Label</label>
|
|
92
|
+
<InputText
|
|
93
|
+
v-model="form.data.value.label"
|
|
94
|
+
placeholder="Administrator"
|
|
95
|
+
class="w-full"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Inherits -->
|
|
100
|
+
<div class="form-field">
|
|
101
|
+
<label class="font-medium">Inherits From</label>
|
|
102
|
+
<div class="inherits-editor">
|
|
103
|
+
<div v-if="(form.data.value.inherits || []).length > 0" class="inherits-chips">
|
|
104
|
+
<Chip
|
|
105
|
+
v-for="roleName in form.data.value.inherits"
|
|
106
|
+
:key="roleName"
|
|
107
|
+
removable
|
|
108
|
+
@remove="removeRole(roleName)"
|
|
109
|
+
class="inherits-chip"
|
|
110
|
+
>
|
|
111
|
+
<span class="chip-name">{{ roleName }}</span>
|
|
112
|
+
<span class="chip-label">({{ getRoleLabel(roleName) }})</span>
|
|
113
|
+
</Chip>
|
|
114
|
+
</div>
|
|
115
|
+
<AutoComplete
|
|
116
|
+
v-model="roleInput"
|
|
117
|
+
:suggestions="roleSuggestions"
|
|
118
|
+
optionLabel="display"
|
|
119
|
+
placeholder="Type role name..."
|
|
120
|
+
:minLength="0"
|
|
121
|
+
completeOnFocus
|
|
122
|
+
@complete="searchRoles"
|
|
123
|
+
@item-select="onRoleSelect"
|
|
124
|
+
dropdown
|
|
125
|
+
class="w-full"
|
|
126
|
+
>
|
|
127
|
+
<template #option="{ option }">
|
|
128
|
+
<div class="role-option">
|
|
129
|
+
<span class="role-name">{{ option.name }}</span>
|
|
130
|
+
<span class="role-label">{{ option.label }}</span>
|
|
131
|
+
</div>
|
|
132
|
+
</template>
|
|
133
|
+
</AutoComplete>
|
|
134
|
+
</div>
|
|
135
|
+
<small class="field-hint">
|
|
136
|
+
This role inherits all permissions from selected roles
|
|
137
|
+
</small>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Permissions -->
|
|
141
|
+
<div class="form-field">
|
|
142
|
+
<label class="font-medium">
|
|
143
|
+
Permissions ({{ (form.data.value.permissions || []).length }})
|
|
144
|
+
</label>
|
|
145
|
+
<PermissionEditor
|
|
146
|
+
v-model="form.data.value.permissions"
|
|
147
|
+
:permissionRegistry="permissionRegistry"
|
|
148
|
+
placeholder="Type namespace:action..."
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</template>
|
|
153
|
+
</FormPage>
|
|
154
|
+
</template>
|
|
155
|
+
|
|
156
|
+
<style scoped>
|
|
157
|
+
/* Form layout - .form-field and .field-hint are global (main.css) */
|
|
158
|
+
.role-form-fields {
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
gap: 1.5rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Inherits editor - uses .editor-box pattern */
|
|
165
|
+
.inherits-editor {
|
|
166
|
+
border: 1px solid var(--p-surface-200);
|
|
167
|
+
border-radius: 0.5rem;
|
|
168
|
+
padding: 0.75rem;
|
|
169
|
+
background: var(--p-surface-50);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Inherits chips - uses .editor-chips pattern */
|
|
173
|
+
.inherits-chips {
|
|
174
|
+
display: flex;
|
|
175
|
+
flex-wrap: wrap;
|
|
176
|
+
gap: 0.5rem;
|
|
177
|
+
margin-bottom: 0.75rem;
|
|
178
|
+
padding-bottom: 0.75rem;
|
|
179
|
+
border-bottom: 1px solid var(--p-surface-200);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.inherits-chip {
|
|
183
|
+
font-family: monospace;
|
|
184
|
+
font-size: 0.875rem;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.chip-name {
|
|
188
|
+
font-weight: 500;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.chip-label {
|
|
192
|
+
color: var(--p-surface-500);
|
|
193
|
+
margin-left: 0.25rem;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.role-option {
|
|
197
|
+
display: flex;
|
|
198
|
+
justify-content: space-between;
|
|
199
|
+
align-items: center;
|
|
200
|
+
gap: 1rem;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.role-name {
|
|
204
|
+
font-family: monospace;
|
|
205
|
+
font-weight: 500;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.role-label {
|
|
209
|
+
color: var(--p-surface-500);
|
|
210
|
+
font-size: 0.875rem;
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* RoleList - Role listing page (standard ListPage pattern)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useListPageBuilder, ListPage, useOrchestrator } from '../../index.js'
|
|
7
|
+
import Column from 'primevue/column'
|
|
8
|
+
import Tag from 'primevue/tag'
|
|
9
|
+
import Chip from 'primevue/chip'
|
|
10
|
+
import Message from 'primevue/message'
|
|
11
|
+
|
|
12
|
+
// ============ LIST BUILDER ============
|
|
13
|
+
const list = useListPageBuilder({ entity: 'roles' })
|
|
14
|
+
|
|
15
|
+
// ============ SEARCH ============
|
|
16
|
+
list.setSearch({
|
|
17
|
+
placeholder: 'Search roles...',
|
|
18
|
+
fields: ['name', 'label']
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// ============ HEADER ACTIONS ============
|
|
22
|
+
list.addCreateAction('New Role')
|
|
23
|
+
|
|
24
|
+
// ============ ROW ACTIONS ============
|
|
25
|
+
list.addEditAction()
|
|
26
|
+
list.addDeleteAction({ labelField: 'label' })
|
|
27
|
+
|
|
28
|
+
// ============ HELPERS ============
|
|
29
|
+
const { getManager } = useOrchestrator()
|
|
30
|
+
const manager = getManager('roles')
|
|
31
|
+
const canPersist = manager?.canPersist ?? false
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<ListPage v-bind="list.props.value" v-on="list.events">
|
|
36
|
+
<!-- Read-only Warning -->
|
|
37
|
+
<template #before-table>
|
|
38
|
+
<Message v-if="!canPersist" severity="info" :closable="false" class="mb-4">
|
|
39
|
+
<div class="flex align-items-center gap-2">
|
|
40
|
+
<i class="pi pi-info-circle text-xl"></i>
|
|
41
|
+
<span>
|
|
42
|
+
<strong>Read-only:</strong> Roles are configured statically.
|
|
43
|
+
Use a PersistableRoleGranterAdapter for editing.
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
</Message>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<template #columns>
|
|
50
|
+
<Column field="name" header="Role" sortable style="width: 25%">
|
|
51
|
+
<template #body="{ data }">
|
|
52
|
+
<div>
|
|
53
|
+
<code class="role-name">{{ data.name }}</code>
|
|
54
|
+
<div v-if="data.label && data.label !== data.name" class="text-sm text-color-secondary mt-1">
|
|
55
|
+
{{ data.label }}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
</Column>
|
|
60
|
+
|
|
61
|
+
<Column header="Inherits" style="width: 20%">
|
|
62
|
+
<template #body="{ data }">
|
|
63
|
+
<div v-if="data.inherits?.length" class="flex flex-wrap gap-1">
|
|
64
|
+
<Tag
|
|
65
|
+
v-for="parent in data.inherits"
|
|
66
|
+
:key="parent"
|
|
67
|
+
:value="parent"
|
|
68
|
+
severity="secondary"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
<span v-else class="text-color-secondary">-</span>
|
|
72
|
+
</template>
|
|
73
|
+
</Column>
|
|
74
|
+
|
|
75
|
+
<Column header="Permissions" style="width: 40%">
|
|
76
|
+
<template #body="{ data }">
|
|
77
|
+
<div v-if="data.permissions?.length" class="flex flex-wrap gap-1">
|
|
78
|
+
<Chip
|
|
79
|
+
v-for="perm in data.permissions.slice(0, 5)"
|
|
80
|
+
:key="perm"
|
|
81
|
+
:label="perm"
|
|
82
|
+
class="text-xs"
|
|
83
|
+
/>
|
|
84
|
+
<Chip
|
|
85
|
+
v-if="data.permissions.length > 5"
|
|
86
|
+
:label="`+${data.permissions.length - 5} more`"
|
|
87
|
+
class="text-xs"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
<span v-else class="text-color-secondary">No permissions</span>
|
|
91
|
+
</template>
|
|
92
|
+
</Column>
|
|
93
|
+
</template>
|
|
94
|
+
</ListPage>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<style scoped>
|
|
98
|
+
.role-name {
|
|
99
|
+
padding: 0.2rem 0.4rem;
|
|
100
|
+
background: var(--p-surface-100);
|
|
101
|
+
border-radius: 4px;
|
|
102
|
+
font-size: 0.875rem;
|
|
103
|
+
color: var(--p-primary-color);
|
|
104
|
+
font-weight: 500;
|
|
105
|
+
}
|
|
106
|
+
</style>
|
package/src/styles/main.css
CHANGED
|
@@ -20,6 +20,18 @@ html, body, #app {
|
|
|
20
20
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/* Global placeholder styling - light blue-gray for better visibility */
|
|
24
|
+
input::placeholder,
|
|
25
|
+
textarea::placeholder,
|
|
26
|
+
.p-inputtext::placeholder,
|
|
27
|
+
.p-autocomplete-input::placeholder,
|
|
28
|
+
.p-textarea::placeholder,
|
|
29
|
+
.p-select-label.p-placeholder,
|
|
30
|
+
.p-dropdown-label.p-placeholder {
|
|
31
|
+
color: #a0aec0 !important;
|
|
32
|
+
opacity: 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
/* Layout - AppLayout styles are now scoped in AppLayout.vue */
|
|
24
36
|
/* Only keep truly global layout utilities here */
|
|
25
37
|
|
|
@@ -181,13 +193,61 @@ html, body, #app {
|
|
|
181
193
|
flex: 1;
|
|
182
194
|
}
|
|
183
195
|
|
|
184
|
-
/* Field hint */
|
|
196
|
+
/* Field hint - small helper text below inputs */
|
|
185
197
|
.field-hint,
|
|
186
198
|
.form-hint {
|
|
187
|
-
font-size: 0.
|
|
199
|
+
font-size: 0.8rem;
|
|
200
|
+
color: var(--p-surface-400);
|
|
201
|
+
margin-top: 0.25rem;
|
|
202
|
+
display: block;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Field error - validation error text */
|
|
206
|
+
.field-error {
|
|
207
|
+
font-size: 0.8rem;
|
|
208
|
+
color: var(--p-red-500);
|
|
209
|
+
margin-top: 0.25rem;
|
|
210
|
+
display: block;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Editor box - container for complex editors (permissions, scopes, etc.) */
|
|
214
|
+
.editor-box {
|
|
215
|
+
border: 1px solid var(--p-surface-200);
|
|
216
|
+
border-radius: 0.5rem;
|
|
217
|
+
padding: 0.75rem;
|
|
218
|
+
background: var(--p-surface-50);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Chips container with separator */
|
|
222
|
+
.editor-chips {
|
|
223
|
+
display: flex;
|
|
224
|
+
flex-wrap: wrap;
|
|
225
|
+
gap: 0.5rem;
|
|
226
|
+
margin-bottom: 0.75rem;
|
|
227
|
+
padding-bottom: 0.75rem;
|
|
228
|
+
border-bottom: 1px solid var(--p-surface-200);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* Chip typography for code-like content */
|
|
232
|
+
.chip-code {
|
|
233
|
+
font-family: monospace;
|
|
234
|
+
font-size: 0.875rem;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.chip-namespace {
|
|
188
238
|
color: var(--p-surface-500);
|
|
189
239
|
}
|
|
190
240
|
|
|
241
|
+
.chip-action {
|
|
242
|
+
color: var(--p-primary-600);
|
|
243
|
+
font-weight: 500;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.chip-wildcard {
|
|
247
|
+
color: var(--p-orange-500);
|
|
248
|
+
font-weight: 700;
|
|
249
|
+
}
|
|
250
|
+
|
|
191
251
|
/* Info grid for readonly data */
|
|
192
252
|
.info-grid {
|
|
193
253
|
display: grid;
|