Skip to content

Commit 20607d1

Browse files
committed
feat(neon-glass): complete overhaul with granular opacity, bubble glow, theme metadata, and CSS rewrite
- Add neonGlassChatOpacity, neonGlassBubbleGlow, neonGlassApplyReply - Add Import/Export theme functionality with metadata support - Redesign NeonGlassBuilder with HexColorPickerPopOut and live preview - Add ng_* field parsing to theme metadata (ng_color, ng_blur, etc.) - ThemeCatalog now auto-applies neon-glass defaults from themes - Rewrite NeonGlass.css with split chat/sidebar opacity and bubble glow - Add multi-layer glow effect with luminance-based text color - Fix double-glass issue on modals and popups Files changed: NeonGlassBuilder, ThemeCatalogSettings, ThemeManager, ThemeEngine, settings, NeonGlass.css, metadata.ts/test, .gitignore
1 parent 6aeafbc commit 20607d1

9 files changed

Lines changed: 837 additions & 452 deletions

File tree

.gitignore

96 Bytes
Binary file not shown.

src/app/features/settings/cosmetics/NeonGlassBuilder.tsx

Lines changed: 465 additions & 361 deletions
Large diffs are not rendered by default.

src/app/features/settings/cosmetics/ThemeCatalogSettings.tsx

Lines changed: 118 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export type CatalogPreviewRow = ThemePair & {
5757
contrast: SableThemeContrast;
5858
tags: string[];
5959
fullInstallUrl: string;
60+
defaults?: SableThemeMetadata['defaults'];
6061
};
6162

6263
export type LocalPreviewRow = ThemeRemoteFavorite & {
@@ -68,6 +69,7 @@ export type LocalPreviewRow = ThemeRemoteFavorite & {
6869
contrast: SableThemeContrast;
6970
tags: string[];
7071
importedLocal?: boolean;
72+
defaults?: SableThemeMetadata['defaults'];
7173
};
7274

7375
export type CatalogTweakRow = TweakCatalogEntry & {
@@ -355,6 +357,7 @@ export function ThemeCatalogSettings({
355357
contrast,
356358
tags: meta.tags ?? [],
357359
fullInstallUrl,
360+
defaults: meta.defaults,
358361
};
359362
})
360363
);
@@ -484,6 +487,7 @@ export function ThemeCatalogSettings({
484487
contrast,
485488
tags: meta.tags ?? [],
486489
importedLocal: fav.importedLocal,
490+
defaults: meta.defaults,
487491
...(authorTrim ? { author: authorTrim } : {}),
488492
};
489493
return row;
@@ -559,30 +563,81 @@ export function ThemeCatalogSettings({
559563

560564
const applyFavoriteToLight = useCallback(
561565
(row: LocalPreviewRow) => {
562-
patchSettings({
566+
const patch: Partial<Settings> = {
563567
themeRemoteLightFullUrl: row.fullUrl,
564568
themeRemoteLightKind: row.kind,
565-
});
569+
};
570+
if (row.defaults?.neonGlass) {
571+
const ng = row.defaults.neonGlass;
572+
if (ng.primaryColor) patch.neonGlassPrimaryColor = ng.primaryColor;
573+
if (ng.blurRadius !== undefined) patch.neonGlassBlur = ng.blurRadius;
574+
if (ng.bgOpacity !== undefined) patch.neonGlassBgOpacity = ng.bgOpacity;
575+
if (ng.chatOpacity !== undefined) patch.neonGlassChatOpacity = ng.chatOpacity;
576+
if (ng.glowRadius !== undefined) patch.neonGlassGlow = ng.glowRadius;
577+
if (ng.bubbleGlow !== undefined) patch.neonGlassBubbleGlow = ng.bubbleGlow;
578+
if (ng.applySidebar !== undefined) patch.neonGlassApplySidebar = ng.applySidebar;
579+
if (ng.applyChat !== undefined) patch.neonGlassApplyChat = ng.applyChat;
580+
if (ng.applyModals !== undefined) patch.neonGlassApplyModals = ng.applyModals;
581+
if (ng.applyReply !== undefined) patch.neonGlassApplyReply = ng.applyReply;
582+
patch.neonGlassEnabled = true;
583+
} else {
584+
patch.neonGlassEnabled = false;
585+
}
586+
patchSettings(patch);
566587
},
567588
[patchSettings]
568589
);
569590

570591
const applyFavoriteToDark = useCallback(
571592
(row: LocalPreviewRow) => {
572-
patchSettings({
593+
const patch: Partial<Settings> = {
573594
themeRemoteDarkFullUrl: row.fullUrl,
574595
themeRemoteDarkKind: row.kind,
575-
});
596+
};
597+
if (row.defaults?.neonGlass) {
598+
const ng = row.defaults.neonGlass;
599+
if (ng.primaryColor) patch.neonGlassPrimaryColor = ng.primaryColor;
600+
if (ng.blurRadius !== undefined) patch.neonGlassBlur = ng.blurRadius;
601+
if (ng.bgOpacity !== undefined) patch.neonGlassBgOpacity = ng.bgOpacity;
602+
if (ng.chatOpacity !== undefined) patch.neonGlassChatOpacity = ng.chatOpacity;
603+
if (ng.glowRadius !== undefined) patch.neonGlassGlow = ng.glowRadius;
604+
if (ng.bubbleGlow !== undefined) patch.neonGlassBubbleGlow = ng.bubbleGlow;
605+
if (ng.applySidebar !== undefined) patch.neonGlassApplySidebar = ng.applySidebar;
606+
if (ng.applyChat !== undefined) patch.neonGlassApplyChat = ng.applyChat;
607+
if (ng.applyModals !== undefined) patch.neonGlassApplyModals = ng.applyModals;
608+
if (ng.applyReply !== undefined) patch.neonGlassApplyReply = ng.applyReply;
609+
patch.neonGlassEnabled = true;
610+
} else {
611+
patch.neonGlassEnabled = false;
612+
}
613+
patchSettings(patch);
576614
},
577615
[patchSettings]
578616
);
579617

580618
const applyFavoriteToManual = useCallback(
581619
(row: LocalPreviewRow) => {
582-
patchSettings({
620+
const patch: Partial<Settings> = {
583621
themeRemoteManualFullUrl: row.fullUrl,
584622
themeRemoteManualKind: row.kind,
585-
});
623+
};
624+
if (row.defaults?.neonGlass) {
625+
const ng = row.defaults.neonGlass;
626+
if (ng.primaryColor) patch.neonGlassPrimaryColor = ng.primaryColor;
627+
if (ng.blurRadius !== undefined) patch.neonGlassBlur = ng.blurRadius;
628+
if (ng.bgOpacity !== undefined) patch.neonGlassBgOpacity = ng.bgOpacity;
629+
if (ng.chatOpacity !== undefined) patch.neonGlassChatOpacity = ng.chatOpacity;
630+
if (ng.glowRadius !== undefined) patch.neonGlassGlow = ng.glowRadius;
631+
if (ng.bubbleGlow !== undefined) patch.neonGlassBubbleGlow = ng.bubbleGlow;
632+
if (ng.applySidebar !== undefined) patch.neonGlassApplySidebar = ng.applySidebar;
633+
if (ng.applyChat !== undefined) patch.neonGlassApplyChat = ng.applyChat;
634+
if (ng.applyModals !== undefined) patch.neonGlassApplyModals = ng.applyModals;
635+
if (ng.applyReply !== undefined) patch.neonGlassApplyReply = ng.applyReply;
636+
patch.neonGlassEnabled = true;
637+
} else {
638+
patch.neonGlassEnabled = false;
639+
}
640+
patchSettings(patch);
586641
},
587642
[patchSettings]
588643
);
@@ -697,11 +752,28 @@ export function ThemeCatalogSettings({
697752
];
698753
}
699754

700-
patchSettings({
755+
const patch: Partial<Settings> = {
701756
themeRemoteLightFullUrl: row.fullInstallUrl,
702757
themeRemoteLightKind: kind,
703758
themeRemoteFavorites: pruneFavorites(nextFavorites, nextActive),
704-
});
759+
};
760+
if (row.defaults?.neonGlass) {
761+
const ng = row.defaults.neonGlass;
762+
if (ng.primaryColor) patch.neonGlassPrimaryColor = ng.primaryColor;
763+
if (ng.blurRadius !== undefined) patch.neonGlassBlur = ng.blurRadius;
764+
if (ng.bgOpacity !== undefined) patch.neonGlassBgOpacity = ng.bgOpacity;
765+
if (ng.chatOpacity !== undefined) patch.neonGlassChatOpacity = ng.chatOpacity;
766+
if (ng.glowRadius !== undefined) patch.neonGlassGlow = ng.glowRadius;
767+
if (ng.bubbleGlow !== undefined) patch.neonGlassBubbleGlow = ng.bubbleGlow;
768+
if (ng.applySidebar !== undefined) patch.neonGlassApplySidebar = ng.applySidebar;
769+
if (ng.applyChat !== undefined) patch.neonGlassApplyChat = ng.applyChat;
770+
if (ng.applyModals !== undefined) patch.neonGlassApplyModals = ng.applyModals;
771+
if (ng.applyReply !== undefined) patch.neonGlassApplyReply = ng.applyReply;
772+
patch.neonGlassEnabled = true;
773+
} else {
774+
patch.neonGlassEnabled = false;
775+
}
776+
patchSettings(patch);
705777
},
706778
[darkRemoteFullUrl, favorites, manualRemoteFullUrl, patchSettings, prefetchFull, pruneFavorites]
707779
);
@@ -732,11 +804,28 @@ export function ThemeCatalogSettings({
732804
];
733805
}
734806

735-
patchSettings({
807+
const patch: Partial<Settings> = {
736808
themeRemoteDarkFullUrl: row.fullInstallUrl,
737809
themeRemoteDarkKind: kind,
738810
themeRemoteFavorites: pruneFavorites(nextFavorites, nextActive),
739-
});
811+
};
812+
if (row.defaults?.neonGlass) {
813+
const ng = row.defaults.neonGlass;
814+
if (ng.primaryColor) patch.neonGlassPrimaryColor = ng.primaryColor;
815+
if (ng.blurRadius !== undefined) patch.neonGlassBlur = ng.blurRadius;
816+
if (ng.bgOpacity !== undefined) patch.neonGlassBgOpacity = ng.bgOpacity;
817+
if (ng.chatOpacity !== undefined) patch.neonGlassChatOpacity = ng.chatOpacity;
818+
if (ng.glowRadius !== undefined) patch.neonGlassGlow = ng.glowRadius;
819+
if (ng.bubbleGlow !== undefined) patch.neonGlassBubbleGlow = ng.bubbleGlow;
820+
if (ng.applySidebar !== undefined) patch.neonGlassApplySidebar = ng.applySidebar;
821+
if (ng.applyChat !== undefined) patch.neonGlassApplyChat = ng.applyChat;
822+
if (ng.applyModals !== undefined) patch.neonGlassApplyModals = ng.applyModals;
823+
if (ng.applyReply !== undefined) patch.neonGlassApplyReply = ng.applyReply;
824+
patch.neonGlassEnabled = true;
825+
} else {
826+
patch.neonGlassEnabled = false;
827+
}
828+
patchSettings(patch);
740829
},
741830
[
742831
favorites,
@@ -774,11 +863,28 @@ export function ThemeCatalogSettings({
774863
];
775864
}
776865

777-
patchSettings({
866+
const patch: Partial<Settings> = {
778867
themeRemoteManualFullUrl: row.fullInstallUrl,
779868
themeRemoteManualKind: kind,
780869
themeRemoteFavorites: pruneFavorites(nextFavorites, nextActive),
781-
});
870+
};
871+
if (row.defaults?.neonGlass) {
872+
const ng = row.defaults.neonGlass;
873+
if (ng.primaryColor) patch.neonGlassPrimaryColor = ng.primaryColor;
874+
if (ng.blurRadius !== undefined) patch.neonGlassBlur = ng.blurRadius;
875+
if (ng.bgOpacity !== undefined) patch.neonGlassBgOpacity = ng.bgOpacity;
876+
if (ng.chatOpacity !== undefined) patch.neonGlassChatOpacity = ng.chatOpacity;
877+
if (ng.glowRadius !== undefined) patch.neonGlassGlow = ng.glowRadius;
878+
if (ng.bubbleGlow !== undefined) patch.neonGlassBubbleGlow = ng.bubbleGlow;
879+
if (ng.applySidebar !== undefined) patch.neonGlassApplySidebar = ng.applySidebar;
880+
if (ng.applyChat !== undefined) patch.neonGlassApplyChat = ng.applyChat;
881+
if (ng.applyModals !== undefined) patch.neonGlassApplyModals = ng.applyModals;
882+
if (ng.applyReply !== undefined) patch.neonGlassApplyReply = ng.applyReply;
883+
patch.neonGlassEnabled = true;
884+
} else {
885+
patch.neonGlassEnabled = false;
886+
}
887+
patchSettings(patch);
782888
},
783889
[darkRemoteFullUrl, favorites, lightRemoteFullUrl, patchSettings, prefetchFull, pruneFavorites]
784890
);

src/app/pages/ThemeManager.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,13 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
6868
const [neonGlassPrimaryColor] = useSetting(settingsAtom, 'neonGlassPrimaryColor');
6969
const [neonGlassBlur] = useSetting(settingsAtom, 'neonGlassBlur');
7070
const [neonGlassBgOpacity] = useSetting(settingsAtom, 'neonGlassBgOpacity');
71+
const [neonGlassChatOpacity] = useSetting(settingsAtom, 'neonGlassChatOpacity');
7172
const [neonGlassGlow] = useSetting(settingsAtom, 'neonGlassGlow');
73+
const [neonGlassBubbleGlow] = useSetting(settingsAtom, 'neonGlassBubbleGlow');
7274
const [neonGlassApplySidebar] = useSetting(settingsAtom, 'neonGlassApplySidebar');
7375
const [neonGlassApplyChat] = useSetting(settingsAtom, 'neonGlassApplyChat');
7476
const [neonGlassApplyModals] = useSetting(settingsAtom, 'neonGlassApplyModals');
77+
const [neonGlassApplyReply] = useSetting(settingsAtom, 'neonGlassApplyReply');
7578

7679
useEffect(() => {
7780
document.body.className = '';
@@ -105,10 +108,13 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
105108
primaryColor: neonGlassPrimaryColor,
106109
blurRadius: neonGlassBlur,
107110
bgOpacity: neonGlassBgOpacity,
111+
chatOpacity: neonGlassChatOpacity,
108112
glowRadius: neonGlassGlow,
113+
bubbleGlow: neonGlassBubbleGlow,
109114
applySidebar: neonGlassApplySidebar,
110115
applyChat: neonGlassApplyChat,
111116
applyModals: neonGlassApplyModals,
117+
applyReply: neonGlassApplyReply,
112118
});
113119
} else {
114120
ThemeEngine.resetNeonGlass();
@@ -118,10 +124,13 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
118124
neonGlassPrimaryColor,
119125
neonGlassBlur,
120126
neonGlassBgOpacity,
127+
neonGlassChatOpacity,
121128
neonGlassGlow,
129+
neonGlassBubbleGlow,
122130
neonGlassApplySidebar,
123131
neonGlassApplyChat,
124132
neonGlassApplyModals,
133+
neonGlassApplyReply,
125134
]);
126135

127136
useEffect(() => {

src/app/services/ThemeEngine.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,27 @@ export interface NeonGlassPrefs {
88
primaryColor: string;
99
blurRadius: number;
1010
bgOpacity: number;
11+
chatOpacity: number;
1112
glowRadius: number;
13+
bubbleGlow: number;
1214
applySidebar: boolean;
1315
applyChat: boolean;
1416
applyModals: boolean;
17+
applyReply: boolean;
1518
enableTransition?: boolean;
1619
}
1720

1821
export const NEON_GLASS_DEFAULTS: NeonGlassPrefs = {
1922
primaryColor: '#00f0ff',
2023
blurRadius: 14,
2124
bgOpacity: 0.42,
25+
chatOpacity: 0.15,
2226
glowRadius: 12,
27+
bubbleGlow: 4,
2328
applySidebar: true,
2429
applyChat: true,
2530
applyModals: true,
31+
applyReply: true,
2632
enableTransition: true,
2733
};
2834

@@ -35,7 +41,9 @@ class ThemeEngineService {
3541
const primary = this.sanitizeHexColor(prefs.primaryColor) ?? NEON_GLASS_DEFAULTS.primaryColor;
3642
const blur = this.sanitizeNumber(prefs.blurRadius, 0, 32) ?? NEON_GLASS_DEFAULTS.blurRadius;
3743
const opacity = this.sanitizeNumber(prefs.bgOpacity, 0.05, 1.0) ?? NEON_GLASS_DEFAULTS.bgOpacity;
44+
const chatOp = this.sanitizeNumber(prefs.chatOpacity, 0.0, 1.0) ?? NEON_GLASS_DEFAULTS.chatOpacity;
3845
const glow = this.sanitizeNumber(prefs.glowRadius, 0, 30) ?? NEON_GLASS_DEFAULTS.glowRadius;
46+
const bubbleGlow = this.sanitizeNumber(prefs.bubbleGlow, 0, 20) ?? NEON_GLASS_DEFAULTS.bubbleGlow;
3947
const shouldTransition = prefs.enableTransition ?? NEON_GLASS_DEFAULTS.enableTransition;
4048

4149
// Enable transition for smooth activation
@@ -44,19 +52,30 @@ class ThemeEngineService {
4452
}
4553

4654
root.style.setProperty('--sable-primary-main', primary);
47-
root.style.setProperty('--sable-primary-on-main', '#ffffff');
48-
55+
4956
const rgb = this.hexToRgb(primary);
50-
if (rgb) root.style.setProperty('--sable-primary-main-rgb', rgb);
57+
if (rgb) {
58+
root.style.setProperty('--sable-primary-main-rgb', rgb);
59+
const [r, g, b] = rgb.split(',').map(Number);
60+
// Calculate relative luminance to decide text color (black or white)
61+
const luminance = (0.299 * (r ?? 0) + 0.587 * (g ?? 0) + 0.114 * (b ?? 0)) / 255;
62+
root.style.setProperty('--sable-primary-on-main', luminance > 0.5 ? '#000000' : '#ffffff');
63+
}
5164

5265
root.style.setProperty('--ng-blur', `${blur}px`);
5366
root.style.setProperty('--ng-opacity', String(opacity));
54-
root.style.setProperty('--ng-glow', `0 0 ${glow}px ${primary}`);
67+
root.style.setProperty('--ng-chat-opacity', String(chatOp));
68+
69+
// Create a vibrant, multi-layered glow.
70+
const spread = glow > 0 ? glow / 5 : 0;
71+
root.style.setProperty('--ng-glow', `0 0 ${glow}px ${spread}px ${primary}, 0 0 ${glow / 2}px ${primary}`);
72+
root.style.setProperty('--ng-bubble-glow', `0 0 ${bubbleGlow}px ${primary}`);
5573

5674
document.body.dataset.neonGlass = 'true';
5775
document.body.dataset.ngSidebar = String(prefs.applySidebar ?? NEON_GLASS_DEFAULTS.applySidebar);
5876
document.body.dataset.ngChat = String(prefs.applyChat ?? NEON_GLASS_DEFAULTS.applyChat);
5977
document.body.dataset.ngModals = String(prefs.applyModals ?? NEON_GLASS_DEFAULTS.applyModals);
78+
document.body.dataset.ngReply = String(prefs.applyReply ?? NEON_GLASS_DEFAULTS.applyReply);
6079

6180
// Clean up transition after completion
6281
if (this.transitionTimeout) clearTimeout(this.transitionTimeout);
@@ -72,7 +91,7 @@ class ThemeEngineService {
7291
resetNeonGlass(): void {
7392
try {
7493
const root = document.documentElement;
75-
94+
7695
// Enable smooth transition for reset
7796
document.body.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
7897

@@ -81,12 +100,15 @@ class ThemeEngineService {
81100
root.style.removeProperty('--sable-primary-main-rgb');
82101
root.style.removeProperty('--ng-blur');
83102
root.style.removeProperty('--ng-opacity');
103+
root.style.removeProperty('--ng-chat-opacity');
84104
root.style.removeProperty('--ng-glow');
85-
105+
root.style.removeProperty('--ng-bubble-glow');
106+
86107
delete document.body.dataset.neonGlass;
87108
delete document.body.dataset.ngSidebar;
88109
delete document.body.dataset.ngChat;
89110
delete document.body.dataset.ngModals;
111+
delete document.body.dataset.ngReply;
90112

91113
// Clean up transition
92114
if (this.transitionTimeout) clearTimeout(this.transitionTimeout);

src/app/state/settings.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,13 @@ export interface Settings {
127127
neonGlassEnabled: boolean;
128128
neonGlassPrimaryColor?: string;
129129
neonGlassBlur?: number;
130-
neonGlassBgOpacity?: number;
130+
neonGlassChatOpacity?: number;
131131
neonGlassGlow?: number;
132+
neonGlassBubbleGlow?: number;
132133
neonGlassApplySidebar: boolean;
133134
neonGlassApplyChat: boolean;
134135
neonGlassApplyModals: boolean;
136+
neonGlassApplyReply: boolean;
135137

136138
// Sable features!
137139
sendPresence: boolean;
@@ -260,10 +262,13 @@ export const defaultSettings: Settings = {
260262
neonGlassPrimaryColor: '#00f0ff',
261263
neonGlassBlur: 14,
262264
neonGlassBgOpacity: 0.42,
265+
neonGlassChatOpacity: 0.15,
263266
neonGlassGlow: 12,
267+
neonGlassBubbleGlow: 4,
264268
neonGlassApplySidebar: true,
265269
neonGlassApplyChat: true,
266270
neonGlassApplyModals: true,
271+
neonGlassApplyReply: true,
267272

268273
// Sable features!
269274
sendPresence: true,

0 commit comments

Comments
 (0)