@adea-ai/ui 0.35.0 → 0.37.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.
@@ -19,6 +19,21 @@ export type SurfacePreference = 'opaque' | 'frosted' | 'translucent';
19
19
  export declare const APPEARANCE_STORAGE_KEY = "appearance";
20
20
  /** The pre-#425 key. Migration reads it and never deletes it. */
21
21
  export declare const LEGACY_THEME_STORAGE_KEY = "theme";
22
+ /**
23
+ * Recovery envelope for unread preference documents (Dev Runtime spec,
24
+ * "Compatibility, migrations, and waivers": a failed migration retains the
25
+ * original record; it never silently rewrites or deletes the input). A
26
+ * malformed or future-version document is quarantined here — before any
27
+ * write can touch the main key — so saving valid preferences later can never
28
+ * destroy the user's unread data.
29
+ */
30
+ export declare const APPEARANCE_RECOVERY_STORAGE_KEY = "appearance.recovery";
31
+ export type AppearanceRecoveryEnvelope = Readonly<{
32
+ schemaVersion: 1;
33
+ reason: 'corrupt_json' | 'unsupported_record';
34
+ capturedAt: string;
35
+ raw: string;
36
+ }>;
22
37
  export declare const DARK_QUERY = "(prefers-color-scheme: dark)";
23
38
  export declare const REDUCED_TRANSPARENCY_QUERY = "(prefers-reduced-transparency: reduce)";
24
39
  export declare const defaultAppearancePreferences: AppearancePreferencesV2;
@@ -197,8 +212,9 @@ export declare function migrateLegacyThemeValue(stored: string | null): Appearan
197
212
  type AppearanceStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
198
213
  /**
199
214
  * Read the appearance preferences: the v2 key first, then the legacy `theme`
200
- * key, then defaults. Storage failures degrade to defaults like every other
201
- * blocked-storage consumer.
215
+ * key, then defaults. A malformed or future-version document is quarantined
216
+ * into the recovery envelope and the defaults are returned; storage failures
217
+ * degrade to defaults like every other blocked-storage consumer.
202
218
  */
203
219
  export declare function readAppearancePreferences(storage: AppearanceStorage | undefined): AppearancePreferencesV2;
204
220
  export declare function writeAppearancePreferences(storage: AppearanceStorage | undefined, preferences: AppearancePreferencesV2): void;
@@ -2,6 +2,15 @@
2
2
  var APPEARANCE_STORAGE_KEY = "appearance";
3
3
  /** The pre-#425 key. Migration reads it and never deletes it. */
4
4
  var LEGACY_THEME_STORAGE_KEY = "theme";
5
+ /**
6
+ * Recovery envelope for unread preference documents (Dev Runtime spec,
7
+ * "Compatibility, migrations, and waivers": a failed migration retains the
8
+ * original record; it never silently rewrites or deletes the input). A
9
+ * malformed or future-version document is quarantined here — before any
10
+ * write can touch the main key — so saving valid preferences later can never
11
+ * destroy the user's unread data.
12
+ */
13
+ var APPEARANCE_RECOVERY_STORAGE_KEY = "appearance.recovery";
5
14
  var DARK_QUERY = "(prefers-color-scheme: dark)";
6
15
  var REDUCED_TRANSPARENCY_QUERY = "(prefers-reduced-transparency: reduce)";
7
16
  var defaultAppearancePreferences = Object.freeze({
@@ -599,9 +608,27 @@ function migrateLegacyThemeValue(stored) {
599
608
  };
600
609
  }
601
610
  /**
611
+ * Quarantine an unread raw document into the recovery envelope. Runs at read
612
+ * time — before any later write can overwrite the main key — and is
613
+ * idempotent: re-reading the same unread value refreshes the capture without
614
+ * losing it.
615
+ */
616
+ function retainRecoveryEnvelope(storage, raw, reason) {
617
+ try {
618
+ const envelope = {
619
+ schemaVersion: 1,
620
+ reason,
621
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
622
+ raw
623
+ };
624
+ storage.setItem(APPEARANCE_RECOVERY_STORAGE_KEY, JSON.stringify(envelope));
625
+ } catch {}
626
+ }
627
+ /**
602
628
  * Read the appearance preferences: the v2 key first, then the legacy `theme`
603
- * key, then defaults. Storage failures degrade to defaults like every other
604
- * blocked-storage consumer.
629
+ * key, then defaults. A malformed or future-version document is quarantined
630
+ * into the recovery envelope and the defaults are returned; storage failures
631
+ * degrade to defaults like every other blocked-storage consumer.
605
632
  */
606
633
  function readAppearancePreferences(storage) {
607
634
  if (!storage) return defaultAppearancePreferences;
@@ -612,9 +639,12 @@ function readAppearancePreferences(storage) {
612
639
  try {
613
640
  parsed = JSON.parse(raw);
614
641
  } catch {
642
+ retainRecoveryEnvelope(storage, raw, "corrupt_json");
615
643
  return defaultAppearancePreferences;
616
644
  }
617
- return normalizeAppearancePreferences(parsed).value;
645
+ const normalized = normalizeAppearancePreferences(parsed);
646
+ if (normalized.retainedRaw !== void 0) retainRecoveryEnvelope(storage, raw, typeof parsed === "object" && parsed !== null ? "unsupported_record" : "corrupt_json");
647
+ return normalized.value;
618
648
  }
619
649
  return migrateLegacyThemeValue(storage.getItem("theme")) ?? defaultAppearancePreferences;
620
650
  } catch {
@@ -765,4 +795,4 @@ function kebabCase(value) {
765
795
  return value.replaceAll(/[A-Z]/g, (character) => `-${character.toLocaleLowerCase()}`);
766
796
  }
767
797
  //#endregion
768
- export { APPEARANCE_STORAGE_KEY, ColorParseError, DARK_QUERY, LEGACY_THEME_STORAGE_KEY, REDUCED_TRANSPARENCY_QUERY, accentPresetById, accentPresets, appearanceThemeScript, applyAppearanceToDocument, builtinThemeRegistry, colorToHex, contrastRatio, defaultAppearancePreferences, deriveAccentRoles, flatVariantTokens, migrateLegacyThemeValue, normalizeAccentValue, normalizeAppearancePreferences, parseColor, readAppearancePreferences, resolveAppearanceMode, resolveAppearanceState, resolveSurface, resolveThemeVariant, themeSelectionVariantId, validateThemeRegistry, writeAppearancePreferences };
798
+ export { APPEARANCE_RECOVERY_STORAGE_KEY, APPEARANCE_STORAGE_KEY, ColorParseError, DARK_QUERY, LEGACY_THEME_STORAGE_KEY, REDUCED_TRANSPARENCY_QUERY, accentPresetById, accentPresets, appearanceThemeScript, applyAppearanceToDocument, builtinThemeRegistry, colorToHex, contrastRatio, defaultAppearancePreferences, deriveAccentRoles, flatVariantTokens, migrateLegacyThemeValue, normalizeAccentValue, normalizeAppearancePreferences, parseColor, readAppearancePreferences, resolveAppearanceMode, resolveAppearanceState, resolveSurface, resolveThemeVariant, themeSelectionVariantId, validateThemeRegistry, writeAppearancePreferences };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "Shared UI kit for Adea: Kobalte/corvu-backed Solid primitives, workspace shells, and theme styles.",
5
5
  "keywords": [
6
6
  "components",
@@ -41,6 +41,22 @@ export type SurfacePreference = 'opaque' | 'frosted' | 'translucent'
41
41
  export const APPEARANCE_STORAGE_KEY = 'appearance'
42
42
  /** The pre-#425 key. Migration reads it and never deletes it. */
43
43
  export const LEGACY_THEME_STORAGE_KEY = 'theme'
44
+ /**
45
+ * Recovery envelope for unread preference documents (Dev Runtime spec,
46
+ * "Compatibility, migrations, and waivers": a failed migration retains the
47
+ * original record; it never silently rewrites or deletes the input). A
48
+ * malformed or future-version document is quarantined here — before any
49
+ * write can touch the main key — so saving valid preferences later can never
50
+ * destroy the user's unread data.
51
+ */
52
+ export const APPEARANCE_RECOVERY_STORAGE_KEY = 'appearance.recovery'
53
+
54
+ export type AppearanceRecoveryEnvelope = Readonly<{
55
+ schemaVersion: 1
56
+ reason: 'corrupt_json' | 'unsupported_record'
57
+ capturedAt: string
58
+ raw: string
59
+ }>
44
60
 
45
61
  export const DARK_QUERY = '(prefers-color-scheme: dark)'
46
62
  export const REDUCED_TRANSPARENCY_QUERY = '(prefers-reduced-transparency: reduce)'
@@ -837,10 +853,35 @@ export function migrateLegacyThemeValue(
837
853
 
838
854
  type AppearanceStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>
839
855
 
856
+ /**
857
+ * Quarantine an unread raw document into the recovery envelope. Runs at read
858
+ * time — before any later write can overwrite the main key — and is
859
+ * idempotent: re-reading the same unread value refreshes the capture without
860
+ * losing it.
861
+ */
862
+ function retainRecoveryEnvelope(
863
+ storage: AppearanceStorage,
864
+ raw: string,
865
+ reason: AppearanceRecoveryEnvelope['reason']
866
+ ): void {
867
+ try {
868
+ const envelope: AppearanceRecoveryEnvelope = {
869
+ schemaVersion: 1,
870
+ reason,
871
+ capturedAt: new Date().toISOString(),
872
+ raw,
873
+ }
874
+ storage.setItem(APPEARANCE_RECOVERY_STORAGE_KEY, JSON.stringify(envelope))
875
+ } catch {
876
+ // Quarantine is best-effort; the active preference still fails closed.
877
+ }
878
+ }
879
+
840
880
  /**
841
881
  * Read the appearance preferences: the v2 key first, then the legacy `theme`
842
- * key, then defaults. Storage failures degrade to defaults like every other
843
- * blocked-storage consumer.
882
+ * key, then defaults. A malformed or future-version document is quarantined
883
+ * into the recovery envelope and the defaults are returned; storage failures
884
+ * degrade to defaults like every other blocked-storage consumer.
844
885
  */
845
886
  export function readAppearancePreferences(
846
887
  storage: AppearanceStorage | undefined
@@ -853,9 +894,18 @@ export function readAppearancePreferences(
853
894
  try {
854
895
  parsed = JSON.parse(raw)
855
896
  } catch {
897
+ retainRecoveryEnvelope(storage, raw, 'corrupt_json')
856
898
  return defaultAppearancePreferences
857
899
  }
858
- return normalizeAppearancePreferences(parsed).value
900
+ const normalized = normalizeAppearancePreferences(parsed)
901
+ if (normalized.retainedRaw !== undefined) {
902
+ retainRecoveryEnvelope(
903
+ storage,
904
+ raw,
905
+ typeof parsed === 'object' && parsed !== null ? 'unsupported_record' : 'corrupt_json'
906
+ )
907
+ }
908
+ return normalized.value
859
909
  }
860
910
  return (
861
911
  migrateLegacyThemeValue(storage.getItem(LEGACY_THEME_STORAGE_KEY)) ??
@@ -791,3 +791,98 @@
791
791
  transition: none;
792
792
  }
793
793
  }
794
+
795
+ /* Session deep-link recovery: a visible, announced banner above the body. */
796
+ .dev-recovery-banner {
797
+ margin: 0;
798
+ padding: 0.5rem 1.25rem;
799
+ border-block-end: 1px solid var(--dev-border);
800
+ color: var(--dev-muted);
801
+ font-size: 0.8125rem;
802
+ background: var(--dev-surface-muted, transparent);
803
+ }
804
+
805
+ /* Provider-backed archive shelf content. */
806
+ .dev-archive-shelf__content {
807
+ max-block-size: 14rem;
808
+ overflow-y: auto;
809
+ border-block-start: 1px solid var(--dev-border);
810
+ padding-block-end: 0.5rem;
811
+ }
812
+
813
+ .dev-archive-shelf__list {
814
+ margin: 0;
815
+ padding: 0;
816
+ list-style: none;
817
+ }
818
+
819
+ .dev-archive-shelf__item {
820
+ display: flex;
821
+ align-items: center;
822
+ gap: 0.5rem;
823
+ padding: 0.35rem 0.75rem;
824
+ font-size: 0.8125rem;
825
+ }
826
+
827
+ .dev-archive-shelf__item .dev-tree-row__title {
828
+ min-width: 0;
829
+ flex: 1;
830
+ overflow: hidden;
831
+ text-overflow: ellipsis;
832
+ white-space: nowrap;
833
+ }
834
+
835
+ .dev-archive-shelf__actions {
836
+ display: flex;
837
+ align-items: center;
838
+ gap: 0.25rem;
839
+ flex: none;
840
+ }
841
+
842
+ .dev-archive-shelf__confirm {
843
+ display: flex;
844
+ align-items: center;
845
+ gap: 0.375rem;
846
+ font-size: 0.75rem;
847
+ color: var(--dev-muted);
848
+ }
849
+
850
+ .dev-archive-action {
851
+ border: 1px solid var(--dev-border);
852
+ border-radius: 0.375rem;
853
+ background: transparent;
854
+ color: inherit;
855
+ padding: 0.125rem 0.5rem;
856
+ font-size: 0.75rem;
857
+ cursor: pointer;
858
+ }
859
+
860
+ .dev-archive-action:hover {
861
+ background: color-mix(in srgb, var(--foreground) 8%, transparent);
862
+ }
863
+
864
+ .dev-archive-action--destructive {
865
+ color: var(--destructive);
866
+ border-color: color-mix(in srgb, var(--destructive) 40%, transparent);
867
+ }
868
+
869
+ .dev-archive-shelf__handoff {
870
+ margin: 0;
871
+ padding: 0.5rem 0.75rem;
872
+ font-size: 0.75rem;
873
+ line-height: 1.45;
874
+ color: var(--dev-muted);
875
+ border-block-start: 1px solid var(--dev-border);
876
+ }
877
+
878
+ /* Truthful per-pane provider states inside the utility slots. */
879
+ .dev-pane-state {
880
+ padding: 0.75rem 0.875rem;
881
+ }
882
+
883
+ .dev-pane-state__line {
884
+ margin: 0;
885
+ font-size: 0.8125rem;
886
+ line-height: 1.5;
887
+ color: var(--dev-muted);
888
+ }