diff --git a/frontend/src/AppDevelopmentPreview.contract.test.ts b/frontend/src/AppDevelopmentPreview.contract.test.ts
index 26e599a..7e8fa4b 100644
--- a/frontend/src/AppDevelopmentPreview.contract.test.ts
+++ b/frontend/src/AppDevelopmentPreview.contract.test.ts
@@ -84,6 +84,18 @@ describe('browser development preview contract', () => {
/\.phone-home-indicator:focus-visible span\s*\{[^}]*0 0 0 2px #0a84ff,/s,
)
})
+
+ it('replaces rectangular CEF focus outlines on the side hardware controls', () => {
+ expect(mainCss).toMatch(
+ /\.phone-hardware-button:focus\s*\{[^}]*outline:\s*none;/s,
+ )
+ expect(mainCss).toMatch(
+ /\.phone-hardware-button:focus-visible::after\s*\{[^}]*width:\s*3px;[^}]*height:\s*24px;[^}]*border-radius:\s*999px;[^}]*background:\s*#0a84ff;/s,
+ )
+ expect(mainCss).not.toMatch(
+ /\.phone-hardware-button:focus-visible\s*\{[^}]*outline:/s,
+ )
+ })
it('consumes Escape synchronously before FiveM can open the pause menu', () => {
expect(source).toContain("import { consumeEscape } from '@/utils/keyboard'")
diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css
index 69214a3..a9dbbbb 100644
--- a/frontend/src/assets/main.css
+++ b/frontend/src/assets/main.css
@@ -380,9 +380,27 @@ button {
.phone-hardware-button:disabled {
cursor: default;
}
-.phone-hardware-button:focus-visible {
- outline: 2px solid #fff;
- outline-offset: -6px;
+.phone-hardware-button:focus {
+ outline: none;
+}
+.phone-hardware-button:focus-visible::after {
+ position: absolute;
+ top: 50%;
+ width: 3px;
+ height: 24px;
+ border-radius: 999px;
+ background: #0a84ff;
+ box-shadow: 0 0 0 1px rgb(255 255 255 / 90%);
+ content: '';
+ transform: translateY(-50%);
+}
+.phone-hardware-button--action:focus-visible::after,
+.phone-hardware-button--volume-up:focus-visible::after,
+.phone-hardware-button--volume-down:focus-visible::after {
+ right: 7px;
+}
+.phone-hardware-button--power:focus-visible::after {
+ left: 7px;
}
.phone-hardware-button--action {
top: 176px;
diff --git a/frontend/src/components/AdminConfigValueEditor.vue b/frontend/src/components/AdminConfigValueEditor.vue
index 4c5e2a0..d07875b 100644
--- a/frontend/src/components/AdminConfigValueEditor.vue
+++ b/frontend/src/components/AdminConfigValueEditor.vue
@@ -18,6 +18,7 @@ import type { AdminConfiguratorDescribe } from '@/utils/adminConfiguratorDescrip
export type AdminConfigEditorLabels = {
addField: string
+ addJob: string
addRow: string
configuredSecret: string
convertToList: string
@@ -27,6 +28,7 @@ export type AdminConfigEditorLabels = {
emptyTable: string
entry: string
general: string
+ jobPlaceholder: string
keyPlaceholder: string
list: string
remove: string
@@ -149,6 +151,11 @@ const canExtendTable = computed(
!vectorType.value &&
(!tableStructure.value || tableStructure.value.mutableKeys === true),
)
+const isJobTable = computed(
+ () =>
+ props.path === 'Radio.DisplayName.AllowedJobs' ||
+ /^Radio\.LockedChannels\[\d+\]\.jobs$/.test(props.path),
+)
const usesFixedTableLayout = computed(
() =>
Boolean(tableStructure.value) && tableStructure.value?.mutableKeys !== true,
@@ -371,6 +378,19 @@ function updateOptionalString(event: Event): void {
}
}
+function updateNewObjectKey(event: Event): void {
+ const target = event.target
+ if (!(target instanceof HTMLInputElement)) return
+ const value = isJobTable.value
+ ? target.value
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]/g, '')
+ .slice(0, 64)
+ : target.value
+ newObjectKey.value = value
+ target.value = value
+}
+
function toggleOptionalString(event: Event): void {
const target = event.target
if (!(target instanceof HTMLInputElement)) return
@@ -1015,11 +1035,15 @@ function mapEntryStructure(
@submit.prevent="addTableField"
>