stellar-drive 1.1.26 → 1.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.
@@ -206,7 +206,7 @@ function generatePackageJson(opts) {
206
206
  },
207
207
  dependencies: {
208
208
  postgres: '^3.4.0',
209
- 'stellar-drive': '^1.1.26'
209
+ 'stellar-drive': '^1.2.0'
210
210
  },
211
211
  type: 'module'
212
212
  }, null, 2) + '\n');
@@ -2060,6 +2060,7 @@ function generateSetupPageSvelte(opts) {
2060
2060
  import { setConfig } from 'stellar-drive/config';
2061
2061
  import { isOnline } from 'stellar-drive/stores';
2062
2062
  import { pollForNewServiceWorker } from 'stellar-drive/kit';
2063
+ import Reconfigure from './Reconfigure.svelte';
2063
2064
 
2064
2065
  // =============================================================================
2065
2066
  // Wizard State
@@ -2261,6 +2262,7 @@ function generateSetupPageSvelte(opts) {
2261
2262
 
2262
2263
  <div class="setup-page">
2263
2264
  <div class="setup-container">
2265
+ {#if isFirstSetup}
2264
2266
  <!-- Header -->
2265
2267
  <h1>Set Up ${opts.name}</h1>
2266
2268
  <p class="subtitle">Configure ${opts.name} to connect to your own Supabase backend</p>
@@ -2395,7 +2397,7 @@ function generateSetupPageSvelte(opts) {
2395
2397
  </div>
2396
2398
  <div class="deploy-stage" class:active={deployStage === 'deploying'} class:done={deployStage === 'ready'}>
2397
2399
  <span class="stage-icon">{#if deployStage === 'deploying'}&#9675;{:else if deployStage === 'ready'}&#10003;{:else}&#8226;{/if}</span>
2398
- Deploying to Vercel
2400
+ Deploying to Vercel... (might take a minute)
2399
2401
  </div>
2400
2402
  <div class="deploy-stage" class:active={deployStage === 'ready'}>
2401
2403
  <span class="stage-icon">{#if deployStage === 'ready'}&#10003;{:else}&#8226;{/if}</span>
@@ -2406,8 +2408,16 @@ function generateSetupPageSvelte(opts) {
2406
2408
 
2407
2409
  {#if deployStage === 'ready'}
2408
2410
  <div class="message message-success">
2409
- Deployment complete! <a href="/">Refresh to start using ${opts.name}</a>.
2411
+ Deployment complete! Use the update prompt at the bottom of the screen to refresh.
2412
+ If it doesn't appear, click below.
2410
2413
  </div>
2414
+ <button
2415
+ class="btn btn-secondary"
2416
+ onclick={() => (window.location.href = '/')}
2417
+ style="margin-top: 0.75rem;"
2418
+ >
2419
+ Manually refresh &amp; go home
2420
+ </button>
2411
2421
  {/if}
2412
2422
  {/if}
2413
2423
  </div>
@@ -2432,12 +2442,16 @@ function generateSetupPageSvelte(opts) {
2432
2442
  </div>
2433
2443
 
2434
2444
  <!-- Security notice (first-time setup only) -->
2435
- {#if isFirstSetup}
2436
2445
  <div class="security-notice">
2437
2446
  <strong>Security:</strong> Your Supabase credentials are stored as environment variables
2438
2447
  on Vercel and are never sent to any third-party service. The Vercel token is used once
2439
2448
  and is not persisted.
2440
2449
  </div>
2450
+ {:else}
2451
+ <!-- Reconfigure view for returning users -->
2452
+ <h1>Reconfigure ${opts.name}</h1>
2453
+ <p class="subtitle">Update your credentials and redeploy</p>
2454
+ <Reconfigure />
2441
2455
  {/if}
2442
2456
  </div>
2443
2457
  </div>
@@ -2691,6 +2705,732 @@ function generateSetupPageSvelte(opts) {
2691
2705
  </style>
2692
2706
  `;
2693
2707
  }
2708
+ /**
2709
+ * Generate the Reconfigure component for the setup page.
2710
+ *
2711
+ * Shown when `isFirstSetup: false` — a flat settings page where the
2712
+ * user can update Supabase credentials and redeploy without stepping
2713
+ * through the full wizard.
2714
+ *
2715
+ * @returns The Svelte component source for `src/routes/setup/Reconfigure.svelte`.
2716
+ */
2717
+ function generateReconfigureSvelte(opts) {
2718
+ return `<!--
2719
+ @fileoverview Reconfigure settings page for ${opts.name}.
2720
+
2721
+ Shown when \\\`isFirstSetup: false\\\` — a flat settings page where the
2722
+ user can update Supabase credentials and redeploy without stepping
2723
+ through the full wizard.
2724
+ -->
2725
+ <script lang="ts">
2726
+ import { getConfig, setConfig } from 'stellar-drive/config';
2727
+ import { isOnline } from 'stellar-drive/stores';
2728
+ import { pollForNewServiceWorker, monitorSwLifecycle } from 'stellar-drive/kit';
2729
+ import { onMount } from 'svelte';
2730
+ import { browser } from '$app/environment';
2731
+
2732
+ // ===========================================================================
2733
+ // Form State
2734
+ // ===========================================================================
2735
+
2736
+ let supabaseUrl = $state('');
2737
+ let supabasePublishableKey = $state('');
2738
+ let vercelToken = $state('');
2739
+
2740
+ // Initial values for change detection
2741
+ let initialSupabaseUrl = $state('');
2742
+ let initialSupabaseKey = $state('');
2743
+
2744
+ // ===========================================================================
2745
+ // UI State
2746
+ // ===========================================================================
2747
+
2748
+ let loading = $state(true);
2749
+ let validating = $state(false);
2750
+ let validateError = $state<string | null>(null);
2751
+ let validateSuccess = $state(false);
2752
+ let validatedUrl = $state('');
2753
+ let validatedKey = $state('');
2754
+ let deploying = $state(false);
2755
+ let deployError = $state<string | null>(null);
2756
+ let deployStage = $state<'idle' | 'setting-env' | 'deploying' | 'ready'>('idle');
2757
+
2758
+ // ===========================================================================
2759
+ // Derived State
2760
+ // ===========================================================================
2761
+
2762
+ const supabaseChanged = $derived(
2763
+ supabaseUrl !== initialSupabaseUrl || supabasePublishableKey !== initialSupabaseKey
2764
+ );
2765
+
2766
+ const credentialsChanged = $derived(
2767
+ validateSuccess && (supabaseUrl !== validatedUrl || supabasePublishableKey !== validatedKey)
2768
+ );
2769
+
2770
+ const supabaseNeedsValidation = $derived(supabaseChanged && !validateSuccess);
2771
+
2772
+ const canDeploy = $derived(
2773
+ supabaseChanged &&
2774
+ !supabaseNeedsValidation &&
2775
+ !credentialsChanged &&
2776
+ !!vercelToken &&
2777
+ !deploying &&
2778
+ deployStage === 'idle'
2779
+ );
2780
+
2781
+ // ===========================================================================
2782
+ // Effects
2783
+ // ===========================================================================
2784
+
2785
+ $effect(() => {
2786
+ if (credentialsChanged) {
2787
+ validateSuccess = false;
2788
+ validateError = null;
2789
+ }
2790
+ });
2791
+
2792
+ // ===========================================================================
2793
+ // Lifecycle
2794
+ // ===========================================================================
2795
+
2796
+ onMount(() => {
2797
+ if (!browser) return;
2798
+
2799
+ const config = getConfig();
2800
+ if (config) {
2801
+ supabaseUrl = config.supabaseUrl || '';
2802
+ supabasePublishableKey = config.supabasePublishableKey || '';
2803
+ initialSupabaseUrl = supabaseUrl;
2804
+ initialSupabaseKey = supabasePublishableKey;
2805
+ }
2806
+
2807
+ loading = false;
2808
+ });
2809
+
2810
+ // ===========================================================================
2811
+ // Validation
2812
+ // ===========================================================================
2813
+
2814
+ async function handleValidate() {
2815
+ validateError = null;
2816
+ validateSuccess = false;
2817
+ validating = true;
2818
+
2819
+ try {
2820
+ const res = await fetch('/api/setup/validate', {
2821
+ method: 'POST',
2822
+ headers: { 'Content-Type': 'application/json' },
2823
+ body: JSON.stringify({ supabaseUrl, supabasePublishableKey })
2824
+ });
2825
+
2826
+ const data = await res.json();
2827
+
2828
+ if (data.valid) {
2829
+ validateSuccess = true;
2830
+ validatedUrl = supabaseUrl;
2831
+ validatedKey = supabasePublishableKey;
2832
+ setConfig({ supabaseUrl, supabasePublishableKey, configured: true });
2833
+ } else {
2834
+ validateError = data.error || 'Validation failed';
2835
+ }
2836
+ } catch (e) {
2837
+ validateError = e instanceof Error ? e.message : 'Network error';
2838
+ }
2839
+
2840
+ validating = false;
2841
+ }
2842
+
2843
+ // ===========================================================================
2844
+ // Deployment
2845
+ // ===========================================================================
2846
+
2847
+ function pollForDeployment(): Promise<void> {
2848
+ return new Promise((resolve) => {
2849
+ let resolved = false;
2850
+
2851
+ const done = () => {
2852
+ if (resolved) return;
2853
+ resolved = true;
2854
+ stopPoll();
2855
+ stopMonitor();
2856
+ deployStage = 'ready';
2857
+ resolve();
2858
+ };
2859
+
2860
+ const stopMonitor = monitorSwLifecycle({ onUpdateAvailable: done });
2861
+
2862
+ const stopPoll = pollForNewServiceWorker({
2863
+ intervalMs: 3000,
2864
+ maxAttempts: 200,
2865
+ onFound: done
2866
+ });
2867
+
2868
+ if (typeof navigator !== 'undefined' && navigator.serviceWorker) {
2869
+ navigator.serviceWorker.addEventListener('controllerchange', done, { once: true });
2870
+ }
2871
+
2872
+ setTimeout(() => {
2873
+ if (!resolved) done();
2874
+ }, 180_000);
2875
+ });
2876
+ }
2877
+
2878
+ async function handleDeploy() {
2879
+ deployError = null;
2880
+ deploying = true;
2881
+ deployStage = 'setting-env';
2882
+
2883
+ try {
2884
+ const res = await fetch('/api/setup/deploy', {
2885
+ method: 'POST',
2886
+ headers: { 'Content-Type': 'application/json' },
2887
+ body: JSON.stringify({ supabaseUrl, supabasePublishableKey, vercelToken })
2888
+ });
2889
+
2890
+ const data = await res.json();
2891
+
2892
+ if (data.success) {
2893
+ deployStage = 'deploying';
2894
+ await pollForDeployment();
2895
+ } else {
2896
+ deployError = data.error || 'Deployment failed';
2897
+ deployStage = 'idle';
2898
+ }
2899
+ } catch (e) {
2900
+ deployError = e instanceof Error ? e.message : 'Network error';
2901
+ deployStage = 'idle';
2902
+ }
2903
+
2904
+ deploying = false;
2905
+ }
2906
+ </script>
2907
+
2908
+ <div class="reconfigure-page">
2909
+ {#if loading}
2910
+ <div class="loading-state">
2911
+ <span class="loading-spinner"></span>
2912
+ Loading configuration...
2913
+ </div>
2914
+ {:else}
2915
+ <!-- Supabase Connection Card -->
2916
+ <section class="config-card">
2917
+ <div class="card-header">
2918
+ <h2>Supabase Connection</h2>
2919
+ {#if !supabaseChanged && initialSupabaseUrl}
2920
+ <span class="status-badge status-connected">Connected</span>
2921
+ {/if}
2922
+ </div>
2923
+
2924
+ <p class="card-description">
2925
+ Find these values in your Supabase dashboard under <strong>Settings &gt; API</strong>.
2926
+ </p>
2927
+
2928
+ <div class="form-group">
2929
+ <label for="reconfig-supabase-url">Supabase URL</label>
2930
+ <input
2931
+ id="reconfig-supabase-url"
2932
+ type="url"
2933
+ placeholder="https://your-project.supabase.co"
2934
+ bind:value={supabaseUrl}
2935
+ autocomplete="off"
2936
+ spellcheck="false"
2937
+ />
2938
+ </div>
2939
+
2940
+ <div class="form-group">
2941
+ <label for="reconfig-supabase-key">Supabase Publishable Key</label>
2942
+ <input
2943
+ id="reconfig-supabase-key"
2944
+ type="text"
2945
+ placeholder="eyJhbGciOiJIUzI1NiIs..."
2946
+ bind:value={supabasePublishableKey}
2947
+ autocomplete="off"
2948
+ spellcheck="false"
2949
+ />
2950
+ <span class="input-hint"
2951
+ >This is your public (anon) key. Row-Level Security policies enforce access control.</span
2952
+ >
2953
+ </div>
2954
+
2955
+ <button
2956
+ class="btn btn-secondary"
2957
+ onclick={handleValidate}
2958
+ disabled={!supabaseUrl || !supabasePublishableKey || validating}
2959
+ >
2960
+ {#if validating}
2961
+ <span class="loading-spinner small"></span>
2962
+ Testing connection...
2963
+ {:else}
2964
+ Test Connection
2965
+ {/if}
2966
+ </button>
2967
+
2968
+ {#if validateError}
2969
+ <div class="message error">{validateError}</div>
2970
+ {/if}
2971
+ {#if validateSuccess && !credentialsChanged}
2972
+ <div class="message success">Connection successful — credentials are valid.</div>
2973
+ {/if}
2974
+ </section>
2975
+
2976
+ <!-- Deploy Section -->
2977
+ <section class="config-card">
2978
+ <div class="card-header">
2979
+ <h2>Deploy Changes</h2>
2980
+ </div>
2981
+
2982
+ {#if !$isOnline}
2983
+ <div class="message error">
2984
+ You are currently offline. Deployment requires an internet connection.
2985
+ </div>
2986
+ {/if}
2987
+
2988
+ <div class="form-group">
2989
+ <label for="reconfig-vercel-token">Vercel API Token</label>
2990
+ <input
2991
+ id="reconfig-vercel-token"
2992
+ type="password"
2993
+ placeholder="Paste your Vercel token"
2994
+ bind:value={vercelToken}
2995
+ autocomplete="off"
2996
+ disabled={deploying || deployStage !== 'idle'}
2997
+ />
2998
+ <span class="input-hint">
2999
+ Create a token at
3000
+ <a href="https://vercel.com/account/tokens" target="_blank" rel="noopener noreferrer">
3001
+ vercel.com/account/tokens</a
3002
+ >. It is used once and never stored.
3003
+ </span>
3004
+ </div>
3005
+
3006
+ {#if deployStage === 'idle'}
3007
+ <button class="btn btn-primary" onclick={handleDeploy} disabled={!canDeploy}>
3008
+ {#if deploying}
3009
+ <span class="loading-spinner small"></span>
3010
+ Deploying...
3011
+ {:else}
3012
+ Deploy Changes
3013
+ {/if}
3014
+ </button>
3015
+ {/if}
3016
+
3017
+ {#if deployError}
3018
+ <div class="message error">{deployError}</div>
3019
+ {/if}
3020
+
3021
+ {#if deployStage !== 'idle'}
3022
+ <div class="deploy-steps">
3023
+ <div
3024
+ class="deploy-step"
3025
+ class:active={deployStage === 'setting-env'}
3026
+ class:complete={deployStage === 'deploying' || deployStage === 'ready'}
3027
+ >
3028
+ <div class="deploy-step-indicator">
3029
+ {#if deployStage === 'setting-env'}
3030
+ <span class="loading-spinner small"></span>
3031
+ {:else}
3032
+ <svg
3033
+ width="16"
3034
+ height="16"
3035
+ viewBox="0 0 24 24"
3036
+ fill="none"
3037
+ stroke="currentColor"
3038
+ stroke-width="3"
3039
+ stroke-linecap="round"
3040
+ stroke-linejoin="round"
3041
+ >
3042
+ <polyline points="20 6 9 17 4 12" />
3043
+ </svg>
3044
+ {/if}
3045
+ </div>
3046
+ <span>Setting environment variables...</span>
3047
+ </div>
3048
+
3049
+ <div
3050
+ class="deploy-step"
3051
+ class:active={deployStage === 'deploying'}
3052
+ class:complete={deployStage === 'ready'}
3053
+ >
3054
+ <div class="deploy-step-indicator">
3055
+ {#if deployStage === 'deploying'}
3056
+ <span class="loading-spinner small"></span>
3057
+ {:else if deployStage === 'ready'}
3058
+ <svg
3059
+ width="16"
3060
+ height="16"
3061
+ viewBox="0 0 24 24"
3062
+ fill="none"
3063
+ stroke="currentColor"
3064
+ stroke-width="3"
3065
+ stroke-linecap="round"
3066
+ stroke-linejoin="round"
3067
+ >
3068
+ <polyline points="20 6 9 17 4 12" />
3069
+ </svg>
3070
+ {:else}
3071
+ <div class="deploy-dot"></div>
3072
+ {/if}
3073
+ </div>
3074
+ <span>Deploying... (might take a bit)</span>
3075
+ </div>
3076
+
3077
+ <div class="deploy-step" class:active={deployStage === 'ready'}>
3078
+ <div class="deploy-step-indicator">
3079
+ {#if deployStage === 'ready'}
3080
+ <svg
3081
+ width="16"
3082
+ height="16"
3083
+ viewBox="0 0 24 24"
3084
+ fill="none"
3085
+ stroke="currentColor"
3086
+ stroke-width="3"
3087
+ stroke-linecap="round"
3088
+ stroke-linejoin="round"
3089
+ >
3090
+ <polyline points="20 6 9 17 4 12" />
3091
+ </svg>
3092
+ {:else}
3093
+ <div class="deploy-dot"></div>
3094
+ {/if}
3095
+ </div>
3096
+ <span>Ready</span>
3097
+ </div>
3098
+ </div>
3099
+
3100
+ {#if deployStage === 'ready'}
3101
+ <div class="message success">
3102
+ Deployment complete! Use the update prompt at the bottom of the screen to refresh. If it
3103
+ doesn't appear, click below.
3104
+ </div>
3105
+ <button
3106
+ class="btn btn-secondary"
3107
+ onclick={() => (window.location.href = '/')}
3108
+ style="margin-top: 0.75rem;"
3109
+ >
3110
+ Manually refresh &amp; go home
3111
+ </button>
3112
+ {/if}
3113
+ {/if}
3114
+ </section>
3115
+ {/if}
3116
+ </div>
3117
+
3118
+ <style>
3119
+ /* ===========================================================================
3120
+ Layout
3121
+ =========================================================================== */
3122
+
3123
+ .reconfigure-page {
3124
+ max-width: 640px;
3125
+ width: 100%;
3126
+ display: flex;
3127
+ flex-direction: column;
3128
+ gap: 1.5rem;
3129
+ }
3130
+
3131
+ .loading-state {
3132
+ display: flex;
3133
+ align-items: center;
3134
+ justify-content: center;
3135
+ gap: 0.75rem;
3136
+ padding: 3rem;
3137
+ font-size: 0.9375rem;
3138
+ color: var(--color-text-muted, #666);
3139
+ }
3140
+
3141
+ /* ===========================================================================
3142
+ Config Card
3143
+ =========================================================================== */
3144
+
3145
+ .config-card {
3146
+ padding: 1.5rem;
3147
+ background: var(--color-surface, #fff);
3148
+ border: 1px solid var(--color-border, #e2e2e2);
3149
+ border-radius: 12px;
3150
+ }
3151
+
3152
+ .card-header {
3153
+ display: flex;
3154
+ align-items: center;
3155
+ justify-content: space-between;
3156
+ margin-bottom: 1rem;
3157
+ }
3158
+
3159
+ .card-header h2 {
3160
+ margin: 0;
3161
+ font-size: 1.0625rem;
3162
+ font-weight: 700;
3163
+ color: var(--color-text, #111);
3164
+ }
3165
+
3166
+ .card-description {
3167
+ margin: 0 0 1rem;
3168
+ font-size: 0.875rem;
3169
+ color: var(--color-text-muted, #666);
3170
+ line-height: 1.6;
3171
+ }
3172
+
3173
+ .card-description strong {
3174
+ color: var(--color-text, #111);
3175
+ font-weight: 600;
3176
+ }
3177
+
3178
+ /* ===========================================================================
3179
+ Status Badges
3180
+ =========================================================================== */
3181
+
3182
+ .status-badge {
3183
+ display: inline-flex;
3184
+ align-items: center;
3185
+ gap: 0.25rem;
3186
+ padding: 0.2rem 0.5rem;
3187
+ font-size: 0.6875rem;
3188
+ font-weight: 600;
3189
+ border-radius: 4px;
3190
+ letter-spacing: 0.02em;
3191
+ }
3192
+
3193
+ .status-connected {
3194
+ background: var(--color-success-bg, #f0fdf4);
3195
+ color: var(--color-success, #16a34a);
3196
+ }
3197
+
3198
+ /* ===========================================================================
3199
+ Form Elements
3200
+ =========================================================================== */
3201
+
3202
+ .form-group {
3203
+ display: flex;
3204
+ flex-direction: column;
3205
+ gap: 0.5rem;
3206
+ margin-bottom: 1rem;
3207
+ }
3208
+
3209
+ .form-group label {
3210
+ font-weight: 700;
3211
+ color: var(--color-text-muted, #666);
3212
+ font-size: 0.6875rem;
3213
+ text-transform: uppercase;
3214
+ letter-spacing: 0.1em;
3215
+ }
3216
+
3217
+ .form-group input {
3218
+ width: 100%;
3219
+ padding: 0.75rem 0.875rem;
3220
+ font-size: 0.9375rem;
3221
+ color: var(--color-text, #111);
3222
+ background: var(--color-bg, #fafafa);
3223
+ border: 1px solid var(--color-border, #e2e2e2);
3224
+ border-radius: 8px;
3225
+ transition: border-color 0.2s;
3226
+ font-family: inherit;
3227
+ box-sizing: border-box;
3228
+ }
3229
+
3230
+ .form-group input:focus {
3231
+ outline: none;
3232
+ border-color: var(--color-primary, #3b82f6);
3233
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
3234
+ }
3235
+
3236
+ .form-group input:disabled {
3237
+ opacity: 0.6;
3238
+ cursor: not-allowed;
3239
+ }
3240
+
3241
+ .form-group input::placeholder {
3242
+ color: var(--color-text-muted, #999);
3243
+ opacity: 0.6;
3244
+ }
3245
+
3246
+ .input-hint {
3247
+ font-size: 0.75rem;
3248
+ color: var(--color-text-muted, #666);
3249
+ opacity: 0.7;
3250
+ line-height: 1.4;
3251
+ }
3252
+
3253
+ .input-hint a {
3254
+ color: var(--color-primary, #3b82f6);
3255
+ text-decoration: none;
3256
+ border-bottom: 1px solid rgba(59, 130, 246, 0.3);
3257
+ transition: border-color 0.2s;
3258
+ }
3259
+
3260
+ .input-hint a:hover {
3261
+ border-bottom-color: var(--color-primary, #3b82f6);
3262
+ }
3263
+
3264
+ /* ===========================================================================
3265
+ Messages
3266
+ =========================================================================== */
3267
+
3268
+ .message {
3269
+ padding: 0.875rem 1rem;
3270
+ border-radius: 8px;
3271
+ font-size: 0.875rem;
3272
+ font-weight: 500;
3273
+ line-height: 1.5;
3274
+ margin-top: 0.75rem;
3275
+ }
3276
+
3277
+ .error {
3278
+ background: var(--color-error-bg, #fef2f2);
3279
+ color: var(--color-error, #dc2626);
3280
+ border: 1px solid rgba(220, 38, 38, 0.2);
3281
+ }
3282
+
3283
+ .success {
3284
+ background: var(--color-success-bg, #f0fdf4);
3285
+ color: var(--color-success, #16a34a);
3286
+ border: 1px solid rgba(22, 163, 106, 0.2);
3287
+ }
3288
+
3289
+ /* ===========================================================================
3290
+ Buttons
3291
+ =========================================================================== */
3292
+
3293
+ .btn {
3294
+ display: flex;
3295
+ align-items: center;
3296
+ justify-content: center;
3297
+ gap: 0.5rem;
3298
+ padding: 0.75rem 1.25rem;
3299
+ font-size: 0.9375rem;
3300
+ font-weight: 600;
3301
+ border-radius: 8px;
3302
+ cursor: pointer;
3303
+ transition: all 0.2s;
3304
+ border: none;
3305
+ font-family: inherit;
3306
+ }
3307
+
3308
+ .btn-primary {
3309
+ background: var(--color-primary, #3b82f6);
3310
+ color: white;
3311
+ }
3312
+
3313
+ .btn-primary:hover:not(:disabled) {
3314
+ opacity: 0.9;
3315
+ }
3316
+
3317
+ .btn-secondary {
3318
+ background: var(--color-secondary, #6b7280);
3319
+ color: white;
3320
+ }
3321
+
3322
+ .btn-secondary:hover:not(:disabled) {
3323
+ opacity: 0.9;
3324
+ }
3325
+
3326
+ .btn:disabled {
3327
+ opacity: 0.5;
3328
+ cursor: not-allowed;
3329
+ }
3330
+
3331
+ /* ===========================================================================
3332
+ Loading Spinner
3333
+ =========================================================================== */
3334
+
3335
+ .loading-spinner {
3336
+ width: 18px;
3337
+ height: 18px;
3338
+ border: 2px solid rgba(0, 0, 0, 0.15);
3339
+ border-top-color: var(--color-primary, #3b82f6);
3340
+ border-radius: 50%;
3341
+ animation: spin 0.8s linear infinite;
3342
+ display: inline-block;
3343
+ }
3344
+
3345
+ .loading-spinner.small {
3346
+ width: 14px;
3347
+ height: 14px;
3348
+ border-width: 2px;
3349
+ }
3350
+
3351
+ @keyframes spin {
3352
+ to {
3353
+ transform: rotate(360deg);
3354
+ }
3355
+ }
3356
+
3357
+ /* ===========================================================================
3358
+ Deploy Steps
3359
+ =========================================================================== */
3360
+
3361
+ .deploy-steps {
3362
+ display: flex;
3363
+ flex-direction: column;
3364
+ gap: 0.75rem;
3365
+ margin-top: 1rem;
3366
+ }
3367
+
3368
+ .deploy-step {
3369
+ display: flex;
3370
+ align-items: center;
3371
+ gap: 0.75rem;
3372
+ font-size: 0.875rem;
3373
+ color: var(--color-text-muted, #999);
3374
+ opacity: 0.5;
3375
+ transition: all 0.3s;
3376
+ }
3377
+
3378
+ .deploy-step.active {
3379
+ opacity: 1;
3380
+ color: var(--color-primary, #3b82f6);
3381
+ }
3382
+
3383
+ .deploy-step.complete {
3384
+ opacity: 1;
3385
+ color: var(--color-success, #16a34a);
3386
+ }
3387
+
3388
+ .deploy-step-indicator {
3389
+ width: 24px;
3390
+ height: 24px;
3391
+ display: flex;
3392
+ align-items: center;
3393
+ justify-content: center;
3394
+ flex-shrink: 0;
3395
+ }
3396
+
3397
+ .deploy-dot {
3398
+ width: 8px;
3399
+ height: 8px;
3400
+ background: var(--color-border, #ccc);
3401
+ border-radius: 50%;
3402
+ }
3403
+
3404
+ /* ===========================================================================
3405
+ Responsive
3406
+ =========================================================================== */
3407
+
3408
+ @media (max-width: 640px) {
3409
+ .config-card {
3410
+ padding: 1.25rem;
3411
+ }
3412
+
3413
+ .form-group input {
3414
+ font-size: 16px;
3415
+ }
3416
+ }
3417
+
3418
+ @media (prefers-reduced-motion: reduce) {
3419
+ .loading-spinner {
3420
+ animation: none;
3421
+ }
3422
+
3423
+ .btn {
3424
+ transition: none;
3425
+ }
3426
+
3427
+ .deploy-step {
3428
+ transition: none;
3429
+ }
3430
+ }
3431
+ </style>
3432
+ `;
3433
+ }
2694
3434
  /**
2695
3435
  * Generate a minimal privacy policy page component.
2696
3436
  *
@@ -5278,6 +6018,7 @@ export async function run() {
5278
6018
  ['src/routes/+error.svelte', generateErrorPage(opts)],
5279
6019
  ['src/routes/setup/+page.ts', generateSetupPageTs()],
5280
6020
  ['src/routes/setup/+page.svelte', generateSetupPageSvelte(opts)],
6021
+ ['src/routes/setup/Reconfigure.svelte', generateReconfigureSvelte(opts)],
5281
6022
  ['src/routes/policy/+page.svelte', generatePolicyPage(opts)],
5282
6023
  ['src/routes/login/+page.svelte', generateLoginPage(opts)],
5283
6024
  ['src/routes/confirm/+page.svelte', generateConfirmPage(opts)],