ENH - refine phone setup experience (#12)

This commit is contained in:
smx.pusha
2026-08-20 08:55:03 +02:00
committed by GitHub
parent 3eae2b8d30
commit daa67cfb1e
14 changed files with 776 additions and 389 deletions
+19 -7
View File
@@ -271,7 +271,8 @@ const MIN_PRODUCTION_PHONE_ZOOM = 260 / PHONE_PORTRAIT_WIDTH
const developmentParameters = new URLSearchParams(window.location.search)
const isBrowserPreview =
developmentParameters.has('browserPreview') &&
developmentParameters.get('apiBase')?.startsWith('/') === true
(import.meta.env.DEV ||
developmentParameters.get('apiBase')?.startsWith('/') === true)
const isDevelopment =
import.meta.env.DEV ||
developmentParameters.get('apiBase')?.startsWith('/') === true
@@ -346,6 +347,7 @@ const previousHardwareAlertVolume = ref(75)
const hardwareVolumeHudVisible = ref(false)
const setupPreviewDismissed = ref(false)
const setupDevelopmentSkipped = ref(false)
const setupAppearanceSelected = ref(false)
const pendingUnlockRoute = ref<string | null>(null)
const unlockedServicesLoaded = ref(false)
const controlCenterOpened = ref(false)
@@ -359,6 +361,13 @@ const setupRequired = computed(
developmentParameters.has('setupPreview') &&
!setupPreviewDismissed.value)),
)
const displayedDarkMode = computed(
() =>
phone.isDarkMode &&
(!setupRequired.value ||
setupAppearanceSelected.value ||
phone.preferences.settings.setupStep > 4),
)
const hardwareAlertVolume = computed(() =>
Math.round(
(phone.preferences.settings.notificationVolume +
@@ -572,6 +581,7 @@ function loadUnlockedPhoneData(): void {
function completePhoneSetup(): void {
setupPreviewDismissed.value = true
setupAppearanceSelected.value = false
isLocked.value = false
isUnlocking.value = false
passcodeVisible.value = false
@@ -1624,7 +1634,7 @@ onBeforeUnmount(() => {
<section
class="phone-device"
:class="{
'phone-app--light': !phone.isDarkMode,
'phone-app--light': !displayedDarkMode,
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
}"
:aria-label="phone.t('Common.phone')"
@@ -1674,7 +1684,7 @@ onBeforeUnmount(() => {
class="phone-screen"
:class="{
'phone-screen--app': isAppRoute || isDevelopmentRoute,
'phone-app--light': !phone.isDarkMode,
'phone-app--light': !displayedDarkMode,
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
}"
>
@@ -1711,18 +1721,19 @@ onBeforeUnmount(() => {
</Transition>
<k-app
theme="ios"
:dark="phone.isDarkMode"
:dark="displayedDarkMode"
safe-areas
class="phone-app"
:style="phoneDisplayStyle"
:class="{
dark: phone.isDarkMode,
'phone-app--light': !phone.isDarkMode,
dark: displayedDarkMode,
'phone-app--light': !displayedDarkMode,
'phone-app--messages': route.params.appId === 'messages',
'phone-app--status-light':
WHITE_STATUS_BAR_APP_IDS.has(activeAppId),
'phone-app--status-dark':
DARK_STATUS_BAR_APP_IDS.has(activeAppId),
'phone-app--setup': setupRequired,
[`phone-app--${phone.preferences.settings.graphicsMode}`]: true,
'phone-app--unlocking': isUnlocking,
}"
@@ -1742,7 +1753,7 @@ onBeforeUnmount(() => {
/>
<SkyProvider
class="phone-app-theme"
:dark="phone.isDarkMode"
:dark="displayedDarkMode"
safe-areas
>
<RouterView v-slot="{ Component }">
@@ -1801,6 +1812,7 @@ onBeforeUnmount(() => {
</Transition>
<PhoneSetupAssistant
v-if="setupRequired"
@appearance-selected="setupAppearanceSelected = $event"
@complete="completePhoneSetup"
@skip="skipPhoneSetupForDevelopment"
/>
@@ -9,6 +9,17 @@ const mainCss = readFileSync(
)
describe('browser development preview contract', () => {
it('keeps setup light until the appearance choice has been confirmed', () => {
expect(source).toContain('const displayedDarkMode = computed(')
expect(source).toContain('phone.preferences.settings.setupStep > 4')
expect(source).toContain('setupAppearanceSelected.value')
expect(source).toContain(
'@appearance-selected="setupAppearanceSelected = $event"',
)
expect(source).toContain(':dark="displayedDarkMode"')
expect(source).toContain('dark: displayedDarkMode')
})
it('starts unlocked while preserving an explicit lock screen preview', () => {
expect(source).toContain("developmentParameters.has('lockScreenPreview')")
expect(source).toContain(': !isDevelopment || developmentLockScreenPreview')
@@ -87,6 +98,7 @@ describe('browser development preview contract', () => {
it('fills the dedicated browser embed without clipping the hardware controls', () => {
expect(source).toContain("developmentParameters.has('browserPreview')")
expect(source).toContain('import.meta.env.DEV ||')
expect(source).toContain("'phone-stage--browser-preview': isBrowserPreview")
expect(source).toContain('return (availableScale * 0.94) / PHONE_BASE_SCALE')
expect(mainCss).toMatch(
+31 -322
View File
@@ -904,361 +904,63 @@ button {
}
.wallpaper--midnight {
background:
radial-gradient(circle at 76% 17%, #ffffff 0 0.7px, transparent 1.2px),
radial-gradient(circle at 28% 8%, #9fceff 0 0.6px, transparent 1px),
radial-gradient(circle at 82% 48%, #d8c8ff 0 0.8px, transparent 1.3px),
radial-gradient(
circle at 69% 19%,
#faf7ff 0 3%,
#a57be74d 8%,
transparent 23%
),
conic-gradient(
from 214deg at 63% 35%,
transparent 0 29%,
#7352a51f 36%,
transparent 44%
),
radial-gradient(ellipse at 10% 84%, #1767999e 0, transparent 45%),
linear-gradient(158deg, #050511 0%, #11132d 42%, #120a25 68%, #02040c 100%);
background-size:
43px 47px,
67px 71px,
89px 83px,
auto,
auto,
auto,
auto;
radial-gradient(circle at 78% 12%, rgb(87 103 160 / 18%), transparent 38%),
linear-gradient(160deg, #141724 0%, #090b12 100%);
}
.wallpaper--aurora {
background:
radial-gradient(circle at 17% 14%, #dfffff 0 0.6px, transparent 1.1px),
radial-gradient(circle at 78% 9%, #c6e7ff 0 0.7px, transparent 1.2px),
conic-gradient(
from 218deg at 84% 8%,
transparent 0 18%,
#4effbf70 27%,
#6bf5e220 35%,
transparent 43%
),
conic-gradient(
from 224deg at 62% 26%,
transparent 0 20%,
#27cfd46b 28%,
#695eff42 38%,
transparent 47%
),
radial-gradient(ellipse at 12% 80%, #1860bb9c 0%, transparent 48%),
radial-gradient(ellipse at 76% 20%, #2adfbb6e 0%, transparent 38%),
linear-gradient(155deg, #031715 0%, #08293a 43%, #151643 68%, #040914 100%);
background-size:
59px 61px,
83px 79px,
auto,
auto,
auto,
auto,
auto;
radial-gradient(circle at 74% 18%, rgb(80 188 166 / 22%), transparent 42%),
linear-gradient(155deg, #163c3c 0%, #17263c 56%, #0b101b 100%);
}
.wallpaper--ember {
background:
radial-gradient(
circle at 80% 18%,
#fff4ca 0 1.7%,
#ffbd67 5%,
#ff62335c 17%,
transparent 31%
),
repeating-radial-gradient(
ellipse at 78% 21%,
transparent 0 16px,
#ffb15e12 18px 20px
),
conic-gradient(
from 196deg at 12% 78%,
transparent 0 18%,
#ff3d4c66 28%,
#f3943a29 38%,
transparent 48%
),
conic-gradient(
from 22deg at 78% 90%,
transparent 0 22%,
#b52a755e 31%,
transparent 44%
),
radial-gradient(ellipse at 8% 71%, #c8205c91 0, transparent 46%),
linear-gradient(152deg, #20080a 0%, #4b171d 37%, #3c1537 66%, #090710 100%);
radial-gradient(circle at 82% 14%, rgb(237 142 96 / 28%), transparent 40%),
linear-gradient(155deg, #5b2925 0%, #321b25 58%, #17131b 100%);
}
.wallpaper--ocean {
background:
linear-gradient(116deg, transparent 43%, #c7fbff1c 47%, transparent 51%),
linear-gradient(64deg, transparent 44%, #8defff17 48%, transparent 52%),
radial-gradient(
ellipse at 18% 12%,
#d7ffff 0 1%,
#70e7ff85 12%,
transparent 34%
),
repeating-radial-gradient(
ellipse at 48% 107%,
transparent 0 24px,
#73e8ff2e 27px 30px,
transparent 33px 49px
),
radial-gradient(ellipse at 87% 70%, #056bd1ce 0, transparent 48%),
linear-gradient(
172deg,
#b5f4f8 0%,
#148eb0 22%,
#075b91 54%,
#03284d 78%,
#020c20 100%
);
background-size:
82px 82px,
82px 82px,
auto,
auto,
auto,
auto;
radial-gradient(circle at 22% 10%, rgb(119 203 222 / 28%), transparent 40%),
linear-gradient(175deg, #327e98 0%, #185273 46%, #0b2944 100%);
}
.wallpaper--sunrise {
background:
linear-gradient(18deg, #281c46 0 13%, transparent 13.3%),
linear-gradient(-22deg, #51304a 0 18%, transparent 18.3%),
radial-gradient(
circle at 58% 69%,
#fffbd7 0 5.5%,
#ffd28d 6%,
#ff9f666e 18%,
transparent 35%
),
repeating-linear-gradient(0deg, transparent 0 16px, #ffe2ae0d 17px 18px),
radial-gradient(ellipse at 20% 44%, #f888a360 0, transparent 42%),
linear-gradient(
178deg,
#342b78 0%,
#8a477f 35%,
#ed706f 57%,
#ffc277 72%,
#672e50 100%
);
radial-gradient(circle at 68% 68%, rgb(255 205 153 / 30%), transparent 42%),
linear-gradient(175deg, #6b6488 0%, #b46f78 52%, #d29673 100%);
}
.wallpaper--violet {
background:
radial-gradient(
circle at 78% 14%,
transparent 0 9%,
#e0b6ff35 9.5% 10.3%,
transparent 11%
),
radial-gradient(
circle at 18% 74%,
transparent 0 16%,
#a1a2ff29 16.5% 17.2%,
transparent 18%
),
conic-gradient(
from 202deg at 83% 25%,
transparent 0 17%,
#e48dff91 25%,
#8d63ff32 35%,
transparent 45%
),
linear-gradient(122deg, transparent 36%, #f0c9ff13 39%, transparent 42%),
radial-gradient(circle at 13% 81%, #5555e5c4 0, transparent 46%),
radial-gradient(circle at 72% 28%, #a53dcc70 0, transparent 41%),
linear-gradient(148deg, #11072b 0%, #391164 48%, #20103f 72%, #080519 100%);
radial-gradient(circle at 74% 18%, rgb(164 129 199 / 24%), transparent 42%),
linear-gradient(150deg, #513d69 0%, #302b50 56%, #171828 100%);
}
.wallpaper--forest {
background:
radial-gradient(
ellipse at 82% 12%,
transparent 0 7%,
#c8f49c2c 7.5% 8.2%,
transparent 9%
),
repeating-radial-gradient(
ellipse at -8% 108%,
transparent 0 34px,
#c1e6a218 36px 39px
),
linear-gradient(
135deg,
transparent 42%,
#bfe8a31c 45% 47%,
transparent 50%
),
conic-gradient(
from 55deg at 85% 24%,
transparent 0 24%,
#8dcf7361 31%,
transparent 39%
),
radial-gradient(ellipse at 76% 16%, #b7df8675 0, transparent 36%),
radial-gradient(ellipse at 10% 78%, #0b8b70b8 0, transparent 49%),
linear-gradient(162deg, #061713 0%, #144833 45%, #123326 68%, #040d0b 100%);
background-size:
auto,
auto,
auto,
auto,
auto,
auto,
100% 100%;
radial-gradient(circle at 76% 14%, rgb(126 161 121 / 24%), transparent 42%),
linear-gradient(160deg, #395c4c 0%, #243e35 54%, #13231f 100%);
}
.wallpaper--cobalt {
background:
linear-gradient(
145deg,
transparent 0 47%,
#c4ddff18 48% 49%,
transparent 50%
),
linear-gradient(
35deg,
transparent 0 47%,
#70a5ff12 48% 49%,
transparent 50%
),
radial-gradient(
circle at 76% 25%,
transparent 0 12%,
#62b8ff45 12.5% 13.2%,
transparent 14%
),
conic-gradient(
from 215deg at 80% 28%,
transparent 0 22%,
#52b5ff85 30%,
#5457d62e 41%,
transparent 48%
),
radial-gradient(circle at 14% 78%, #134ed1a3 0, transparent 43%),
linear-gradient(152deg, #041438 0%, #0b3182 43%, #172d82 64%, #030a25 100%);
background-size:
58px 58px,
58px 58px,
auto,
auto,
auto,
auto;
radial-gradient(circle at 72% 16%, rgb(91 142 218 / 28%), transparent 40%),
linear-gradient(155deg, #264f8b 0%, #1b3768 54%, #101e3b 100%);
}
.wallpaper--rose {
background:
radial-gradient(
circle at 16% 18%,
#fff6f3 0 1.5%,
#ffd9df9e 8%,
transparent 27%
),
repeating-radial-gradient(
ellipse at 108% 8%,
transparent 0 24px,
#ffd5e121 26px 28px
),
conic-gradient(
from 214deg at 86% 72%,
transparent 0 18%,
#f078aa8c 27%,
#8d3d7b38 39%,
transparent 48%
),
linear-gradient(138deg, transparent 38%, #fff0f217 43%, transparent 48%),
radial-gradient(circle at 87% 77%, #e84e92a8 0, transparent 43%),
linear-gradient(148deg, #40132f 0%, #8e3159 39%, #b64b70 59%, #351128 100%);
radial-gradient(circle at 24% 12%, rgb(224 154 172 / 26%), transparent 42%),
linear-gradient(155deg, #9a596c 0%, #724354 54%, #412936 100%);
}
.wallpaper--sand {
background:
radial-gradient(
circle at 76% 16%,
#fffce7 0 2.5%,
#ffe5a5a8 9%,
transparent 25%
),
repeating-radial-gradient(
ellipse at -2% 110%,
transparent 0 28px,
#fff3d12b 31px 34px,
transparent 37px 52px
),
conic-gradient(
from 82deg at 110% 76%,
transparent 0 24%,
#ecc37e52 32%,
transparent 43%
),
linear-gradient(115deg, transparent 41%, #fff6d21a 44%, transparent 47%),
radial-gradient(ellipse at 15% 78%, #9f654b9e 0, transparent 48%),
linear-gradient(163deg, #8b5c45 0%, #d4a262 43%, #b87950 68%, #4f302b 100%);
radial-gradient(circle at 76% 14%, rgb(222 190 145 / 25%), transparent 42%),
linear-gradient(160deg, #a17d61 0%, #80604e 56%, #513e36 100%);
}
.wallpaper--graphite {
background:
radial-gradient(circle at 77% 17%, #ffffff42 0 0.8%, transparent 12%),
linear-gradient(
125deg,
transparent 39%,
#ffffff12 40% 42%,
transparent 43%
),
linear-gradient(35deg, transparent 39%, #ffffff0a 40% 42%, transparent 43%),
repeating-linear-gradient(
92deg,
transparent 0 3px,
#ffffff09 4px,
transparent 5px 9px
),
conic-gradient(
from 215deg at 79% 24%,
transparent 0 25%,
#b7c1d045 32%,
transparent 43%
),
radial-gradient(circle at 17% 78%, #51596c73 0, transparent 44%),
linear-gradient(157deg, #06070a 0%, #242832 43%, #171920 67%, #030405 100%);
background-size:
auto,
54px 54px,
54px 54px,
100% 100%,
auto,
auto,
auto;
radial-gradient(circle at 76% 14%, rgb(133 140 151 / 18%), transparent 40%),
linear-gradient(160deg, #363a42 0%, #23262c 54%, #121417 100%);
}
.wallpaper--prism {
background:
linear-gradient(
132deg,
transparent 0 32%,
#ffffff36 32.5% 33.2%,
transparent 34%
),
linear-gradient(
42deg,
transparent 0 56%,
#bffcff25 56.5% 57.2%,
transparent 58%
),
conic-gradient(
from 218deg at 70% 29%,
#ff527ca1,
#ffc75f91,
#4cead2a6,
#4f72ffae,
#c64deaa3,
#ff527ca1
),
conic-gradient(
from 38deg at 16% 78%,
transparent 0 19%,
#4ae3d8a1 28%,
#596cff48 39%,
transparent 48%
),
radial-gradient(circle at 16% 76%, #35d8d399 0, transparent 38%),
linear-gradient(152deg, #120d35 0%, #47205f 46%, #10205a 70%, #041321 100%);
background-blend-mode: screen, screen, screen, screen, screen, normal;
radial-gradient(circle at 72% 16%, rgb(165 151 218 / 24%), transparent 42%),
linear-gradient(150deg, #655b86 0%, #42516f 52%, #293545 100%);
}
.wallpaper--custom {
background-color: #090a0d;
@@ -5475,6 +5177,13 @@ button {
color: #111;
text-shadow: 0 1px 3px #fff8;
}
.phone-app--setup .phone-status-bar {
color: #1d1d1f;
text-shadow: none;
}
.phone-app.dark.phone-app--setup .phone-status-bar {
color: #f5f5f7;
}
* {
scrollbar-width: none;
}
@@ -11,13 +11,18 @@ describe('PhoneSetupAssistant contract', () => {
it('uses first-party controls and exposes the complete setup journey', () => {
expect(source).not.toContain("from 'konsta/vue'")
expect(source).toContain('PhonePasscode')
expect(source).toContain("step === 1")
expect(source).toContain("step === 8")
expect(source).toContain('step === 1')
expect(source).toContain('step === 8')
expect(source).toContain("['performance', 'ultimate']")
expect(source).toContain('setup-mode-preview__card--front')
expect(source).toContain('setup-mode-preview__card--back')
expect(source).toContain('WALLPAPER_IDS')
expect(source).toContain('setAllAppNotifications')
expect(source).toContain('appStore.claimApp')
expect(source).toContain('await phone.completeSetup()')
expect(source).toMatch(
/if \(step\.value === 8\) \{[\s\S]*void finish\(\)[\s\S]*return/,
)
expect(source).toContain(':disabled="setupCompleteBusy"')
expect(source).toContain("phone.t('Setup.ready.saveFailed')")
})
@@ -27,4 +32,30 @@ describe('PhoneSetupAssistant contract', () => {
expect(source).toContain('phone.setSetupStep(step.value)')
expect(source).toContain('@click="moveTo(step - 1)"')
})
it('keeps the development skip control away from setup progress', () => {
expect(source).toContain('v-if="showDevelopmentSkip && step === 0"')
})
it('follows the selected system appearance throughout setup', () => {
expect(source).toContain(
'(appearanceSelected || step > 4) && phone.isDarkMode',
)
expect(source).toContain("emit('appearanceSelected', true)")
expect(source).toContain("phone.setPreference('appearanceMode', 'light')")
expect(source).toContain('.setup-assistant--dark.setup-assistant--step-0')
expect(source).toContain('--setup-background: #000000')
expect(source).toContain('background: var(--setup-background)')
})
it('applies the selected graphics mode to the setup experience immediately', () => {
expect(source).toContain("'setup-assistant--performance':")
expect(source).toContain("'setup-assistant--ultimate':")
expect(source).toContain(
'.setup-assistant--performance .setup-forward-enter-active',
)
expect(source).toContain(
'.setup-assistant--ultimate .setup-mode-stack button',
)
})
})
+505 -27
View File
@@ -9,6 +9,7 @@ import {
Palette,
ShieldCheck,
Signal,
Smartphone,
Sparkles,
Wifi,
} from 'lucide-vue-next'
@@ -30,7 +31,11 @@ import {
type WallpaperId,
} from '@/utils/preferences'
const emit = defineEmits<{ complete: []; skip: [] }>()
const emit = defineEmits<{
appearanceSelected: [selected: boolean]
complete: []
skip: []
}>()
const phone = usePhoneStore()
const account = useAccountStore()
@@ -38,6 +43,10 @@ const appStore = useAppStoreStore()
const step = ref(
Math.min(PHONE_SETUP_LAST_STEP, phone.preferences.settings.setupStep),
)
const appearanceSelected = ref(step.value > 4)
if (step.value === 4) {
phone.setPreference('appearanceMode', 'light')
}
const direction = ref<'back' | 'forward'>('forward')
const accountMode = ref<'login' | 'register'>('login')
const email = ref('')
@@ -93,6 +102,13 @@ const showDevelopmentSkip = import.meta.env.DEV
function moveTo(nextStep: number): void {
direction.value = nextStep < step.value ? 'back' : 'forward'
step.value = Math.min(PHONE_SETUP_LAST_STEP, Math.max(0, nextStep))
if (step.value < 4) {
appearanceSelected.value = false
emit('appearanceSelected', false)
}
if (step.value === 4 && !appearanceSelected.value) {
phone.setPreference('appearanceMode', 'light')
}
phone.setSetupStep(step.value)
}
@@ -105,12 +121,16 @@ function continueSetup(): void {
}
if (step.value === 8) {
for (const appId of selectedApps.value) appStore.claimApp(appId)
void finish()
return
}
moveTo(step.value + 1)
}
function chooseAppearance(mode: AppearanceMode): void {
appearanceSelected.value = true
phone.setPreference('appearanceMode', mode)
emit('appearanceSelected', true)
}
function choosePerformance(mode: GraphicsMode): void {
@@ -227,13 +247,22 @@ function skipSetupForDevelopment(): void {
<template>
<section
class="setup-assistant"
:class="`setup-assistant--step-${step}`"
:class="[
`setup-assistant--step-${step}`,
{
'setup-assistant--dark':
(appearanceSelected || step > 4) && phone.isDarkMode,
'setup-assistant--performance':
phone.preferences.settings.graphicsMode === 'performance',
'setup-assistant--ultimate':
phone.preferences.settings.graphicsMode === 'ultimate',
},
]"
:style="currentWallpaperStyle"
:aria-label="phone.t('Setup.title')"
>
<div class="setup-assistant__aurora" aria-hidden="true"></div>
<button
v-if="showDevelopmentSkip"
v-if="showDevelopmentSkip && step === 0"
type="button"
class="setup-assistant__development-skip"
@click="skipSetupForDevelopment"
@@ -262,15 +291,8 @@ function skipSetupForDevelopment(): void {
<main :key="step" class="setup-assistant__page">
<template v-if="step === 0">
<div class="setup-welcome__hero" aria-hidden="true">
<div class="setup-welcome__greetings">
<span>{{ phone.t('Setup.welcome.hello') }}</span>
<span>{{ phone.t('Setup.welcome.hallo') }}</span>
<span>{{ phone.t('Setup.welcome.bonjour') }}</span>
</div>
<div class="setup-welcome__signature">
<span></span>
<span></span>
<span></span>
<div class="setup-welcome__device">
<Smartphone :size="43" :stroke-width="1.55" />
</div>
</div>
<div class="setup-welcome__copy">
@@ -283,19 +305,6 @@ function skipSetupForDevelopment(): void {
</p>
</div>
<footer class="setup-welcome__footer">
<div class="setup-welcome__privacy">
<span
><ShieldCheck :size="14" />{{
phone.t('Setup.welcome.private')
}}</span
>
<i aria-hidden="true"></i>
<span
><Sparkles :size="14" />{{
phone.t('Setup.welcome.personal')
}}</span
>
</div>
<SkyButton class="setup-assistant__primary" @click="continueSetup">
{{ phone.t('Setup.getStarted') }}
</SkyButton>
@@ -632,7 +641,20 @@ function skipSetupForDevelopment(): void {
<span
class="setup-mode-stack__orb"
:class="`setup-mode-stack__orb--${mode}`"
></span>
aria-hidden="true"
>
<i class="setup-mode-preview__backdrop"></i>
<i
class="setup-mode-preview__card setup-mode-preview__card--back"
></i>
<i
class="setup-mode-preview__card setup-mode-preview__card--front"
></i>
<i
class="setup-mode-preview__line setup-mode-preview__line--wide"
></i>
<i class="setup-mode-preview__line"></i>
</span>
<span
><strong>{{ phone.t(`Apps.settings.${mode}Mode`) }}</strong
><small>{{ phone.t(`Setup.performance.${mode}`) }}</small></span
@@ -2126,6 +2148,462 @@ function skipSetupForDevelopment(): void {
opacity: 0;
transform: translateX(16px);
}
/* Minimal system setup language, matching the restrained iOS setup hierarchy. */
.setup-assistant,
.setup-assistant--step-0 {
--setup-background: #ffffff;
--setup-text: #1d1d1f;
--setup-secondary: #6e6e73;
--setup-tertiary: #8e8e93;
--setup-muted: #aeaeb2;
--setup-surface: #f2f2f7;
--setup-selected-surface: #eef6ff;
--setup-control: #e9e9eb;
--setup-control-fill: #ffffff;
--setup-separator: #d1d1d6;
--setup-blue: #007aff;
--setup-green: #34c759;
--setup-red: #ff3b30;
--setup-orb: #dcecff;
--setup-orb-ultimate: #e7e2ff;
--setup-home-indicator: #1d1d1f;
color: var(--setup-text);
background: var(--setup-background);
}
.setup-assistant--dark,
.setup-assistant--dark.setup-assistant--step-0 {
--setup-background: #000000;
--setup-text: #f5f5f7;
--setup-secondary: #98989d;
--setup-tertiary: #8e8e93;
--setup-muted: #636366;
--setup-surface: #1c1c1e;
--setup-selected-surface: #102a43;
--setup-control: #2c2c2e;
--setup-control-fill: #636366;
--setup-separator: #38383a;
--setup-blue: #0a84ff;
--setup-green: #30d158;
--setup-red: #ff453a;
--setup-orb: #102a43;
--setup-orb-ultimate: #28203d;
--setup-home-indicator: #ffffff;
}
.setup-assistant__development-skip {
top: 48px;
right: 13px;
border: 0;
color: var(--setup-blue);
background: transparent;
font-size: 11px;
font-weight: 600;
}
.setup-assistant__chrome {
gap: 10px;
}
.setup-assistant__back {
border: 0;
color: var(--setup-blue);
background: transparent;
backdrop-filter: none;
}
.setup-assistant__progress {
height: 3px;
background: var(--setup-separator);
}
.setup-assistant__progress span {
background: var(--setup-blue);
}
.setup-assistant__counter {
color: var(--setup-tertiary);
font-weight: 600;
}
.setup-assistant__page h1 {
margin: 12px 0 10px;
color: var(--setup-text);
font-size: 31px;
font-weight: 700;
line-height: 1.08;
letter-spacing: -0.04em;
}
.setup-assistant__lead {
color: var(--setup-secondary);
font-size: 14px;
line-height: 1.42;
}
.setup-assistant__eyebrow,
.setup-welcome__eyebrow {
display: none;
}
.setup-assistant__icon {
width: 72px;
height: 72px;
border: 0;
border-radius: 50%;
color: var(--setup-blue);
background: var(--setup-surface);
box-shadow: none;
backdrop-filter: none;
}
.setup-assistant__icon--signal,
.setup-assistant__icon--cloud,
.setup-assistant__icon--security,
.setup-assistant__icon--appearance,
.setup-assistant__icon--performance,
.setup-assistant__icon--wallpaper,
.setup-assistant__icon--notifications,
.setup-assistant__icon--apps {
color: var(--setup-blue);
}
.setup-assistant__primary {
min-height: 50px;
border-radius: 14px !important;
background: var(--setup-blue) !important;
box-shadow: none !important;
font-weight: 650;
}
.setup-assistant__later {
color: var(--setup-blue);
font-weight: 500;
}
.setup-assistant__notice {
padding: 5px 4px;
border: 0;
color: var(--setup-blue);
background: transparent;
}
.setup-assistant__notice p {
color: var(--setup-secondary);
}
.setup-assistant__error {
color: var(--setup-red);
}
.setup-assistant--step-0 .setup-assistant__page {
padding: 105px 28px 0;
}
.setup-welcome__hero {
width: 94px;
height: 94px;
border-radius: 50%;
background: var(--setup-surface);
}
.setup-welcome__hero::before,
.setup-welcome__hero::after {
content: none;
}
.setup-welcome__device {
display: grid;
width: 94px;
height: 94px;
color: var(--setup-blue);
place-items: center;
}
.setup-welcome__copy {
margin-top: 31px;
}
.setup-welcome__copy h1 {
margin-top: 0;
font-size: 32px;
}
.setup-welcome__footer {
width: calc(100% + 56px);
margin-right: -28px;
margin-left: -28px;
padding: 0 28px 13px;
border: 0;
background: var(--setup-background);
backdrop-filter: none;
}
.setup-welcome__footer .setup-assistant__primary {
margin-top: 0;
box-shadow: none;
}
.setup-welcome__home-indicator {
background: var(--setup-home-indicator);
}
.setup-connectivity-card {
min-height: 142px;
border: 0;
border-radius: 22px;
color: var(--setup-text);
background: var(--setup-surface);
box-shadow: none;
}
.setup-connectivity-card__waves {
display: none;
}
.setup-connectivity-card__label,
.setup-connectivity-card svg {
color: var(--setup-blue);
}
.setup-connectivity-card > span:last-of-type {
color: var(--setup-tertiary);
}
.setup-cloud-hero {
width: 72px;
height: 72px;
border-radius: 50%;
color: var(--setup-blue);
background: var(--setup-surface);
}
.setup-cloud-hero__orbit,
.setup-cloud-hero__glow,
.setup-cloud-hero i {
display: none;
}
.setup-cloud-hero svg {
filter: none;
}
.setup-cloud-identity {
border: 0;
background: var(--setup-surface);
}
.setup-cloud-identity > span {
border: 0;
background: var(--setup-blue);
box-shadow: none;
}
.setup-cloud-identity small,
.setup-cloud-strength,
.setup-cloud-security-note {
color: var(--setup-tertiary);
}
.setup-cloud-identity strong,
.setup-cloud-connected strong {
color: var(--setup-text);
}
.setup-cloud-identity em,
.setup-cloud-suffix {
color: var(--setup-blue);
}
.setup-cloud-fields {
border: 0;
background: var(--setup-surface);
}
.setup-cloud-fields :deep(.sky-field + .sky-field) {
border-top-color: var(--setup-separator);
}
.setup-cloud-fields :deep(.sky-field__label) {
color: var(--setup-tertiary);
}
.setup-cloud-fields :deep(.sky-field__input) {
color: var(--setup-text);
caret-color: var(--setup-blue);
}
.setup-cloud-fields :deep(.sky-field__input::placeholder) {
color: var(--setup-muted);
}
.setup-cloud-strength span {
background: var(--setup-separator);
}
.setup-cloud-strength span.active {
background: var(--setup-blue);
box-shadow: none;
}
.setup-assistant__selector {
background: var(--setup-control);
}
.setup-assistant__selector-indicator {
border: 0;
background: var(--setup-control-fill);
box-shadow: 0 1px 4px rgb(0 0 0 / 14%);
}
.setup-assistant__selector button {
color: var(--setup-secondary);
}
.setup-assistant__selector button.active {
color: var(--setup-text);
}
.setup-cloud-connected {
color: var(--setup-green);
}
.setup-cloud-connected span {
color: var(--setup-secondary);
}
.setup-security-visual {
border: 0;
background: var(--setup-surface);
}
.setup-security-visual span {
background: var(--setup-text);
box-shadow: none;
}
.setup-security-visual svg {
color: var(--setup-muted);
}
.setup-passcode-length button,
.setup-choice-grid button,
.setup-mode-stack button,
.setup-toggle-card,
.setup-app-list button,
.setup-ready__summary span {
border-color: transparent;
color: var(--setup-text);
background: var(--setup-surface);
box-shadow: none;
}
.setup-passcode-length button.selected,
.setup-choice-grid button.selected,
.setup-mode-stack button.selected,
.setup-app-list button.selected {
border-color: var(--setup-blue);
background: var(--setup-selected-surface);
box-shadow: none;
}
.setup-passcode-length small,
.setup-mode-stack small,
.setup-toggle-card small,
.setup-app-list small {
color: var(--setup-secondary);
}
.setup-mode-stack__orb {
position: relative;
overflow: hidden;
isolation: isolate;
border-radius: 18px;
background: var(--setup-orb);
box-shadow: none;
}
.setup-mode-stack__orb--ultimate {
background: var(--setup-orb-ultimate);
box-shadow: none;
}
.setup-mode-preview__backdrop,
.setup-mode-preview__card,
.setup-mode-preview__line {
position: absolute;
display: block;
pointer-events: none;
}
.setup-mode-preview__backdrop {
inset: 0;
background: linear-gradient(145deg, #d9eaff 0%, #b9d8ff 100%);
}
.setup-mode-preview__card {
width: 37px;
height: 26px;
border-radius: 8px;
}
.setup-mode-preview__card--back {
top: 11px;
left: 9px;
background: #ffffff;
}
.setup-mode-preview__card--front {
right: 8px;
bottom: 10px;
background: #e7f1ff;
}
.setup-mode-preview__line {
z-index: 2;
right: 15px;
bottom: 17px;
width: 17px;
height: 3px;
border-radius: 999px;
background: #5d80ad;
}
.setup-mode-preview__line--wide {
bottom: 23px;
width: 24px;
}
.setup-mode-stack__orb--ultimate .setup-mode-preview__backdrop {
background:
radial-gradient(circle at 20% 25%, #f4c7ff 0 12%, transparent 35%),
linear-gradient(145deg, #776de4 0%, #342760 100%);
}
.setup-mode-stack__orb--ultimate .setup-mode-preview__card {
border: 1px solid rgb(255 255 255 / 38%);
background: rgb(255 255 255 / 25%);
box-shadow: 0 7px 14px rgb(31 18 70 / 24%);
backdrop-filter: blur(5px);
}
.setup-mode-stack__orb--ultimate .setup-mode-preview__card--back {
transform: rotate(-8deg);
}
.setup-mode-stack__orb--ultimate .setup-mode-preview__card--front {
background: rgb(255 255 255 / 34%);
transform: rotate(5deg);
}
.setup-mode-stack__orb--ultimate .setup-mode-preview__line {
background: rgb(255 255 255 / 72%);
}
.setup-assistant--dark
.setup-mode-stack__orb--performance
.setup-mode-preview__backdrop {
background: linear-gradient(145deg, #203147 0%, #142133 100%);
}
.setup-assistant--dark
.setup-mode-stack__orb--performance
.setup-mode-preview__card--back {
background: #334965;
}
.setup-assistant--dark
.setup-mode-stack__orb--performance
.setup-mode-preview__card--front {
background: #29405d;
}
.setup-assistant--dark
.setup-mode-stack__orb--performance
.setup-mode-preview__line {
background: #a9c7e9;
}
.setup-mode-stack__check,
.setup-app-list button > i {
border-color: var(--setup-muted);
}
.selected .setup-mode-stack__check,
.setup-app-list button.selected > i {
border-color: var(--setup-blue);
background: var(--setup-blue);
}
.setup-wallpapers button.selected {
border-color: var(--setup-background);
box-shadow: 0 0 0 2px var(--setup-blue);
}
.setup-toggle-card {
border: 0;
}
.setup-toggle-card button {
color: var(--setup-text);
}
.setup-toggle-card button + button {
border-top-color: var(--setup-separator);
}
.setup-toggle-card i {
background: var(--setup-separator);
}
.setup-ready__halo {
width: 112px;
height: 112px;
border: 0;
color: var(--setup-blue);
background: var(--setup-surface);
box-shadow: none;
}
.setup-ready__summary span {
color: var(--setup-blue);
}
.setup-ready__summary b {
color: var(--setup-text);
}
.setup-assistant--performance .setup-forward-enter-active,
.setup-assistant--performance .setup-forward-leave-active,
.setup-assistant--performance .setup-back-enter-active,
.setup-assistant--performance .setup-back-leave-active,
.setup-assistant--performance .setup-assistant__progress span,
.setup-assistant--performance .setup-mode-stack button,
.setup-assistant--performance .setup-mode-stack__orb {
transition-duration: 0.01ms;
}
.setup-assistant--ultimate .setup-mode-stack button,
.setup-assistant--ultimate .setup-mode-stack__orb {
transition:
border-color 0.2s ease,
background-color 0.2s ease,
transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@media (prefers-reduced-motion: reduce) {
.setup-assistant,
.setup-assistant *,
@@ -169,6 +169,9 @@ describe('Sky UI Konsta 5.3 parity contracts', () => {
expect(html).toContain('id="directory-search"')
expect(html).toContain('sky-glass')
expect(html).not.toContain('sky-glass--highlight')
expect(html).toContain('<svg class="sky-searchbar__icon"')
expect(html).toContain('fill-rule="evenodd"')
expect(html).not.toContain('<circle')
expect(html).toContain('sky-searchbar__clear')
expect(html).toContain('aria-label="Clear query"')
expect(html).toContain('sky-searchbar__disable')
+6 -19
View File
@@ -1465,26 +1465,13 @@ label.sky-list-item__row {
}
.sky-searchbar__icon {
width: 14px;
height: 14px;
position: relative;
width: 18px;
height: 18px;
display: block;
flex: none;
border: 1.6px solid var(--sky-muted, rgba(0, 0, 0, 0.55));
border-radius: 50%;
opacity: 0.8;
}
.sky-searchbar__icon::after {
width: 6px;
height: 1.6px;
position: absolute;
right: -5px;
bottom: -2px;
content: '';
border-radius: 2px;
background: var(--sky-muted, rgba(0, 0, 0, 0.55));
transform: rotate(45deg);
transform-origin: left center;
color: var(--sky-muted, rgba(0, 0, 0, 0.55));
fill: currentColor;
opacity: 0.72;
}
.sky-searchbar__input {
+7 -1
View File
@@ -141,7 +141,13 @@ function disable(event: MouseEvent): void {
<label v-if="label" class="sky-visually-hidden" :for="resolvedInputId">
{{ label }}
</label>
<span class="sky-searchbar__icon" aria-hidden="true" />
<svg class="sky-searchbar__icon" aria-hidden="true" viewBox="0 0 24 24">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.5 3a6.5 6.5 0 1 0 3.98 11.64l4.44 4.44a1 1 0 0 0 1.42-1.42l-4.44-4.44A6.5 6.5 0 0 0 9.5 3Zm0 2a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Z"
/>
</svg>
<input
:id="resolvedInputId"
ref="input"
@@ -19,7 +19,17 @@ const mainCss = readFileSync(
new URL('../assets/main.css', import.meta.url),
'utf8',
)
const builtInWallpaperCss = mainCss.slice(
mainCss.indexOf('.wallpaper--midnight'),
mainCss.indexOf('.wallpaper--custom'),
)
describe('Springboard page swipe contract', () => {
it('keeps the built-in wallpapers visually restrained', () => {
expect(builtInWallpaperCss).not.toMatch(/(?:conic|repeating-\w+)-gradient/)
expect(builtInWallpaperCss.match(/radial-gradient/g)).toHaveLength(12)
expect(builtInWallpaperCss.match(/linear-gradient/g)).toHaveLength(12)
})
it('replaces the home status row with the edit controls', () => {
expect(viewSource).toContain("emit('editModeChange', editing)")
expect(viewSource).toContain("emit('editModeChange', false)")
@@ -22,7 +22,7 @@ describe('phone app theme contract', () => {
it('provides the reactive phone theme to every routed app', () => {
expect(appShellSource).toContain('import { SkyProvider }')
expect(appShellSource).toContain('class="phone-app-theme"')
expect(appShellSource).toContain(':dark="phone.isDarkMode"')
expect(appShellSource).toContain(':dark="displayedDarkMode"')
expect(appShellSource).toMatch(
/<SkyProvider[\s\S]*?<RouterView[\s\S]*?<component/,
)
@@ -13,6 +13,10 @@ describe('SettingsApp Sky UI contract', () => {
expect(source).not.toMatch(/<\/?k-[a-z]/)
expect(source).toContain('<SkyAppPage')
expect(source).toContain('<SkyNavbar')
expect(source).toContain('class="settings-navbar"')
expect(source).toContain(
'.settings-navbar.sky-navbar--large.sky-navbar--no-navigation',
)
expect(source).toContain(
":variant=\"activeView === 'root' ? 'large' : 'compact'\"",
)
@@ -64,4 +68,10 @@ describe('SettingsApp Sky UI contract', () => {
expect(source).toContain("phone.t('Apps.settings.keepCloudData')")
expect(source).toContain('phone.resetAfterFactoryReset()')
})
it('provides a non-destructive development preview for the reset progress screen', () => {
expect(source).toContain('import.meta.env.DEV')
expect(source).toContain("has('factoryResetPreview')")
expect(source).toContain('factoryResetProgress.value = 46')
})
})
+116 -10
View File
@@ -740,6 +740,14 @@ async function confirmSimEject(): Promise<void> {
}
onMounted(() => {
if (
import.meta.env.DEV &&
new URLSearchParams(window.location.search).has('factoryResetPreview')
) {
factoryResetting.value = true
factoryResetProgress.value = 46
}
if (route.query.wallpaper === '1') {
activeView.value = 'wallpaper'
wallpaperTarget.value =
@@ -771,6 +779,7 @@ onBeforeUnmount(() => {
:label="phone.t('Apps.settings.name')"
>
<SkyNavbar
class="settings-navbar"
:title="
activeView === 'root' ? phone.t('Apps.settings.name') : activeTitle
"
@@ -1699,17 +1708,8 @@ onBeforeUnmount(() => {
>
<div class="settings-reset-content">
<div class="settings-reset-mark" aria-hidden="true">
<span
class="settings-reset-mark__layer settings-reset-mark__layer--back"
></span>
<span
class="settings-reset-mark__layer settings-reset-mark__layer--middle"
></span>
<div class="settings-reset-mark__face">
<Smartphone :size="35" :stroke-width="1.45" />
<span class="settings-reset-mark__erase">
<i></i><i></i><i></i>
</span>
<Smartphone :size="38" :stroke-width="1.45" />
</div>
</div>
@@ -1872,6 +1872,16 @@ onBeforeUnmount(() => {
</template>
<style scoped>
:deep(.settings-navbar.sky-navbar--large) {
min-height: calc(
var(--sky-navbar-safe-area-top) + var(--sky-navbar-large-title-height)
);
}
:deep(.settings-navbar.sky-navbar--large.sky-navbar--no-navigation) {
padding-top: calc(var(--sky-navbar-safe-area-top) + var(--sky-space-3));
}
.settings-search {
margin-bottom: var(--sky-space-4);
}
@@ -2510,6 +2520,102 @@ onBeforeUnmount(() => {
line-height: 14px;
}
/* Factory reset stays intentionally quiet: white, direct and system-like. */
.settings-reset-hero__icon {
border: 0;
border-radius: 50%;
color: #ff3b30;
background: #f2f2f7;
box-shadow: none;
}
.settings-reset-overlay {
padding: 58px 30px 34px;
color: #1d1d1f;
background: #ffffff;
}
.settings-reset-overlay::before,
.settings-reset-overlay::after {
content: none;
}
.settings-reset-content {
max-width: 300px;
}
.settings-reset-mark {
display: grid;
width: 88px;
height: 88px;
place-items: center;
border-radius: 50%;
background: #f2f2f7;
}
.settings-reset-mark__face {
position: static;
display: grid;
width: 88px;
height: 88px;
border: 0;
border-radius: 50%;
color: #ff3b30;
background: transparent;
box-shadow: none;
place-items: center;
}
.settings-reset-heading {
margin-top: 28px;
}
.settings-reset-heading h2 {
color: #1d1d1f;
font-size: 24px;
font-weight: 700;
letter-spacing: -0.04em;
}
.settings-reset-heading p {
color: #6e6e73;
}
.settings-reset-progress-copy {
margin-top: 40px;
}
.settings-reset-progress-copy strong {
color: #1d1d1f;
font-size: 15px;
}
.settings-reset-progress-copy span {
color: #8e8e93;
}
.settings-reset-progress {
height: 5px;
background: #e5e5ea;
}
.settings-reset-progress > span {
background: #007aff;
box-shadow: none;
}
.settings-reset-detail {
color: #8e8e93;
}
.settings-reset-assurance {
margin-top: 34px;
padding: 14px;
border: 0;
border-radius: 16px;
background: #f2f2f7;
box-shadow: none;
}
.settings-reset-assurance__icon {
border-radius: 50%;
color: #007aff;
background: #e4f1ff;
}
.settings-reset-assurance strong {
color: #1d1d1f;
}
.settings-reset-assurance small {
color: #6e6e73;
}
.settings-reset-warning {
color: #8e8e93;
}
.settings-dialog-button--danger:not(:disabled) {
background: var(--sky-danger);
color: #ffffff;
+10
View File
@@ -10407,6 +10407,16 @@ app.post('/api/:endpoint', (request, response) => {
mockPasscode = ''
mockSecurity = { enabled: false, length: null, lockedUntil: 0 }
for (const key of Object.keys(deviceData)) delete deviceData[key]
Object.assign(deviceData, {
apps: { payload: { claimedApps: [] }, revision: 0 },
settings: {
payload: {
settings: { setupCompleted: false, setupStep: 0 },
version: 1,
},
revision: 0,
},
})
response.json({ success: true })
return
}
+13
View File
@@ -1163,6 +1163,19 @@ async function main() {
await expectSuccess(baseUrl, endpoint)
}
await expectSuccess(baseUrl, 'device:factory-reset')
const resetBootstrap = await expectSuccess(
baseUrl,
'development:bootstrap',
{ _testScenario: 'setupPreview' },
true,
)
assert.equal(
resetBootstrap.device.data.settings.payload.settings.setupCompleted,
false,
'factory reset did not restore a browser-testable setup state',
)
const unknown = await post(baseUrl, 'development:missing-mock', {})
assert.deepEqual(unknown, {
error: 'mock_endpoint_missing',