@@ -36,7 +36,9 @@ interface ComponentTokenDefinition {
3636}
3737
3838type ComponentTokenData = Record < string , ComponentTokenDefinition > ;
39- type ComponentOverrides = Record < string , Record < string , string > > ;
39+ type ComponentVariableOverrides = Record < string , string > ;
40+ type ComponentOverrides = Record < string , ComponentVariableOverrides > ;
41+ type ScopedComponentOverrides = Record < string , ComponentOverrides > ;
4042
4143declare global {
4244 interface Window {
@@ -46,6 +48,7 @@ declare global {
4648}
4749
4850const COMPONENT_STORAGE_KEY = 'design-tokens-component-overrides' ;
51+ const GLOBAL_SCOPE_KEY = '__global__' ;
4952
5053class ComponentStorageAdapter {
5154 private key : string ;
@@ -54,7 +57,7 @@ class ComponentStorageAdapter {
5457 this . key = key ;
5558 }
5659
57- public load ( ) : ComponentOverrides {
60+ public load ( ) : ScopedComponentOverrides {
5861 try {
5962 const raw = localStorage . getItem ( this . key ) ;
6063 if ( ! raw ) return { } ;
@@ -63,33 +66,72 @@ class ComponentStorageAdapter {
6366 return { } ;
6467 }
6568
66- const result : ComponentOverrides = { } ;
67- for ( const [ componentName , values ] of Object . entries ( parsed ) ) {
68- if ( ! values || typeof values !== 'object' || Array . isArray ( values ) ) continue ;
69-
70- const componentOverrides : Record < string , string > = { } ;
71- for ( const [ variable , value ] of Object . entries ( values ) ) {
72- if ( typeof value === 'string' && value . trim ( ) !== '' ) {
73- componentOverrides [ variable ] = value ;
74- }
69+ if ( this . isLegacyComponentOverrides ( parsed as Record < string , unknown > ) ) {
70+ const legacy = this . normalizeComponentOverrides ( parsed as Record < string , unknown > ) ;
71+ if ( Object . keys ( legacy ) . length === 0 ) {
72+ return { } ;
7573 }
7674
77- if ( Object . keys ( componentOverrides ) . length > 0 ) {
78- result [ componentName ] = componentOverrides ;
79- }
75+ return {
76+ [ GLOBAL_SCOPE_KEY ] : legacy ,
77+ } ;
8078 }
8179
82- return result ;
80+ return this . normalizeScopedOverrides ( parsed as Record < string , unknown > ) ;
8381 } catch {
8482 return { } ;
8583 }
8684 }
8785
88- public save ( overrides : ComponentOverrides ) : void {
86+ public save ( overrides : ScopedComponentOverrides ) : void {
87+ const cleaned = this . normalizeScopedOverrides ( overrides as Record < string , unknown > ) ;
88+
89+ if ( Object . keys ( cleaned ) . length === 0 ) {
90+ localStorage . removeItem ( this . key ) ;
91+ return ;
92+ }
93+
94+ localStorage . setItem ( this . key , JSON . stringify ( cleaned ) ) ;
95+ }
96+
97+ private isLegacyComponentOverrides ( input : Record < string , unknown > ) : boolean {
98+ const values = Object . values ( input ) ;
99+ if ( values . length === 0 ) {
100+ return false ;
101+ }
102+
103+ return values . every ( ( value ) => {
104+ if ( ! value || typeof value !== 'object' || Array . isArray ( value ) ) {
105+ return false ;
106+ }
107+
108+ const variableValues = Object . values ( value as Record < string , unknown > ) ;
109+ return variableValues . every ( ( entry ) => typeof entry === 'string' ) ;
110+ } ) ;
111+ }
112+
113+ private normalizeScopedOverrides ( input : Record < string , unknown > ) : ScopedComponentOverrides {
114+ const result : ScopedComponentOverrides = { } ;
115+
116+ for ( const [ scopeKey , scopeValue ] of Object . entries ( input ) ) {
117+ if ( ! scopeValue || typeof scopeValue !== 'object' || Array . isArray ( scopeValue ) ) continue ;
118+
119+ const componentOverrides = this . normalizeComponentOverrides ( scopeValue as Record < string , unknown > ) ;
120+ if ( Object . keys ( componentOverrides ) . length > 0 ) {
121+ result [ scopeKey ] = componentOverrides ;
122+ }
123+ }
124+
125+ return result ;
126+ }
127+
128+ private normalizeComponentOverrides ( input : Record < string , unknown > ) : ComponentOverrides {
89129 const cleaned : ComponentOverrides = { } ;
90130
91- for ( const [ componentName , values ] of Object . entries ( overrides ) ) {
92- const filtered : Record < string , string > = { } ;
131+ for ( const [ componentName , values ] of Object . entries ( input ) ) {
132+ if ( ! values || typeof values !== 'object' || Array . isArray ( values ) ) continue ;
133+
134+ const filtered : ComponentVariableOverrides = { } ;
93135
94136 for ( const [ variable , value ] of Object . entries ( values ) ) {
95137 if ( typeof value === 'string' && value . trim ( ) !== '' ) {
@@ -102,23 +144,19 @@ class ComponentStorageAdapter {
102144 }
103145 }
104146
105- if ( Object . keys ( cleaned ) . length === 0 ) {
106- localStorage . removeItem ( this . key ) ;
107- return ;
108- }
109-
110- localStorage . setItem ( this . key , JSON . stringify ( cleaned ) ) ;
147+ return cleaned ;
111148 }
112149}
113150
114151class ComponentCustomizationTool {
115152 private componentData : ComponentTokenData ;
116153 private tokenLibrary : TokenData ;
117154 private storage : ComponentStorageAdapter ;
118- private overrides : ComponentOverrides ;
155+ private overrides : ScopedComponentOverrides ;
119156 private elementsByComponent = new Map < string , HTMLElement [ ] > ( ) ;
120157 private editableComponents = new Set < string > ( ) ;
121158 private activeComponent : string | null = null ;
159+ private activeScopeKey : string = GLOBAL_SCOPE_KEY ;
122160 private root : HTMLElement | null = null ;
123161 private controlsContainer : HTMLElement | null = null ;
124162 private componentSelect : HTMLSelectElement | null = null ;
@@ -157,9 +195,20 @@ class ComponentCustomizationTool {
157195 private pruneUnknownOverrides ( ) : void {
158196 let hasChanges = false ;
159197
160- for ( const componentName of Object . keys ( this . overrides ) ) {
161- if ( ! this . elementsByComponent . has ( componentName ) || ! this . editableComponents . has ( componentName ) ) {
162- delete this . overrides [ componentName ] ;
198+ for ( const [ scopeKey , scopeOverrides ] of Object . entries ( this . overrides ) ) {
199+ for ( const componentName of Object . keys ( scopeOverrides ) ) {
200+ const isMissingComponent =
201+ ! this . elementsByComponent . has ( componentName ) || ! this . editableComponents . has ( componentName ) ;
202+ const hasContextTarget = this . getElementsForContext ( componentName , scopeKey ) . length > 0 ;
203+
204+ if ( isMissingComponent || ! hasContextTarget ) {
205+ delete this . overrides [ scopeKey ] [ componentName ] ;
206+ hasChanges = true ;
207+ }
208+ }
209+
210+ if ( Object . keys ( this . overrides [ scopeKey ] ) . length === 0 ) {
211+ delete this . overrides [ scopeKey ] ;
163212 hasChanges = true ;
164213 }
165214 }
@@ -170,9 +219,11 @@ class ComponentCustomizationTool {
170219 }
171220
172221 private applySavedOverrides ( ) : void {
173- for ( const [ componentName , componentOverrides ] of Object . entries ( this . overrides ) ) {
174- for ( const [ variable , value ] of Object . entries ( componentOverrides ) ) {
175- this . applyVariable ( componentName , variable , value ) ;
222+ for ( const [ scopeKey , scopeOverrides ] of Object . entries ( this . overrides ) ) {
223+ for ( const [ componentName , componentOverrides ] of Object . entries ( scopeOverrides ) ) {
224+ for ( const [ variable , value ] of Object . entries ( componentOverrides ) ) {
225+ this . applyVariable ( componentName , scopeKey , variable , value ) ;
226+ }
176227 }
177228 }
178229 }
@@ -197,7 +248,10 @@ class ComponentCustomizationTool {
197248 if ( ! isEditable ) continue ;
198249
199250 element . classList . add ( 'db-component-target' ) ;
200- element . dataset . customizeTooltip = `Customize ${ this . getComponentLabel ( componentName ) } ` ;
251+ const scopeLabel = this . getScopeLabel ( this . getScopeKeyForElement ( element ) ) ;
252+ element . dataset . customizeTooltip = scopeLabel
253+ ? `Customize ${ this . getComponentLabel ( componentName ) } (${ scopeLabel } )`
254+ : `Customize ${ this . getComponentLabel ( componentName ) } ` ;
201255
202256 const links = element . querySelectorAll < HTMLAnchorElement > ( 'a[href]' ) ;
203257 for ( const link of links ) {
@@ -221,7 +275,8 @@ class ComponentCustomizationTool {
221275 if ( ! this . root ) return ;
222276
223277 this . activeComponent = componentName ;
224- this . setActiveTarget ( componentName , element ) ;
278+ this . activeScopeKey = this . getScopeKeyForElement ( element ) ;
279+ this . setActiveTarget ( componentName , this . activeScopeKey , element ) ;
225280 if ( this . componentSelect ) {
226281 this . componentSelect . value = componentName ;
227282 }
@@ -283,7 +338,7 @@ class ComponentCustomizationTool {
283338 this . componentSelect . addEventListener ( 'change' , ( ) => {
284339 this . activeComponent = this . componentSelect ?. value || null ;
285340 if ( this . activeComponent ) {
286- this . setActiveTarget ( this . activeComponent ) ;
341+ this . setActiveTarget ( this . activeComponent , this . activeScopeKey ) ;
287342 }
288343 this . renderControls ( ) ;
289344 } ) ;
@@ -300,26 +355,50 @@ class ComponentCustomizationTool {
300355
301356 this . renderControls ( ) ;
302357 if ( this . activeComponent ) {
303- this . setActiveTarget ( this . activeComponent ) ;
358+ this . setActiveTarget ( this . activeComponent , this . activeScopeKey ) ;
304359 }
305360 }
306361
307- private setActiveTarget ( componentName : string , preferredElement ?: HTMLElement ) : void {
362+ private setActiveTarget ( componentName : string , scopeKey : string , preferredElement ?: HTMLElement ) : void {
308363 if ( this . activeTargetElement ) {
309364 this . activeTargetElement . classList . remove ( 'db-component-target--active' ) ;
310365 }
311366
312- const candidates = this . elementsByComponent . get ( componentName ) || [ ] ;
313- const target = preferredElement || candidates [ 0 ] || null ;
367+ const candidates = this . getElementsForContext ( componentName , scopeKey ) ;
368+ const fallbackCandidates = this . elementsByComponent . get ( componentName ) || [ ] ;
369+ const target = preferredElement || candidates [ 0 ] || fallbackCandidates [ 0 ] || null ;
314370 if ( ! target ) {
315371 this . activeTargetElement = null ;
316372 return ;
317373 }
318374
375+ this . activeScopeKey = this . getScopeKeyForElement ( target ) ;
319376 target . classList . add ( 'db-component-target--active' ) ;
320377 this . activeTargetElement = target ;
321378 }
322379
380+ private getScopeKeyForElement ( element : HTMLElement ) : string {
381+ const scope = element . closest < HTMLElement > ( '[data-scope]' ) ?. dataset . scope ?. trim ( ) ;
382+ if ( ! scope ) {
383+ return GLOBAL_SCOPE_KEY ;
384+ }
385+
386+ return `scope:${ scope } ` ;
387+ }
388+
389+ private getScopeLabel ( scopeKey : string ) : string {
390+ if ( scopeKey === GLOBAL_SCOPE_KEY ) {
391+ return '' ;
392+ }
393+
394+ return scopeKey . replace ( / ^ s c o p e : / , '' ) ;
395+ }
396+
397+ private getElementsForContext ( componentName : string , scopeKey : string ) : HTMLElement [ ] {
398+ const elements = this . elementsByComponent . get ( componentName ) || [ ] ;
399+ return elements . filter ( ( element ) => this . getScopeKeyForElement ( element ) === scopeKey ) ;
400+ }
401+
323402 private getSortedComponentNames ( ) : string [ ] {
324403 return Array . from ( this . editableComponents ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
325404 }
@@ -361,9 +440,10 @@ class ComponentCustomizationTool {
361440 body . className = 'db-category__body' ;
362441
363442 for ( const setting of category . settings ) {
364- const currentValue = this . overrides [ this . activeComponent ] ?. [ setting . variable ] || setting . default ;
443+ const currentValue =
444+ this . overrides [ this . activeScopeKey ] ?. [ this . activeComponent ] ?. [ setting . variable ] || setting . default ;
365445 const control = createControl ( setting , currentValue , ( variable , value ) => {
366- this . handleChange ( this . activeComponent as string , variable , value , setting . default ) ;
446+ this . handleChange ( this . activeComponent as string , this . activeScopeKey , variable , value , setting . default ) ;
367447 } ) ;
368448 body . appendChild ( control ) ;
369449 }
@@ -406,51 +486,72 @@ class ComponentCustomizationTool {
406486 return categories ;
407487 }
408488
409- private handleChange ( componentName : string , variable : string , value : string , defaultValue : string ) : void {
410- if ( ! this . overrides [ componentName ] ) {
411- this . overrides [ componentName ] = { } ;
489+ private handleChange (
490+ componentName : string ,
491+ scopeKey : string ,
492+ variable : string ,
493+ value : string ,
494+ defaultValue : string ,
495+ ) : void {
496+ if ( ! this . overrides [ scopeKey ] ) {
497+ this . overrides [ scopeKey ] = { } ;
498+ }
499+
500+ if ( ! this . overrides [ scopeKey ] [ componentName ] ) {
501+ this . overrides [ scopeKey ] [ componentName ] = { } ;
412502 }
413503
414504 if ( ! value || value === defaultValue ) {
415- delete this . overrides [ componentName ] [ variable ] ;
416- this . removeVariable ( componentName , variable ) ;
505+ delete this . overrides [ scopeKey ] [ componentName ] [ variable ] ;
506+ this . removeVariable ( componentName , scopeKey , variable ) ;
417507 } else {
418- this . overrides [ componentName ] [ variable ] = value ;
419- this . applyVariable ( componentName , variable , value ) ;
508+ this . overrides [ scopeKey ] [ componentName ] [ variable ] = value ;
509+ this . applyVariable ( componentName , scopeKey , variable , value ) ;
420510 }
421511
422- if ( Object . keys ( this . overrides [ componentName ] ) . length === 0 ) {
423- delete this . overrides [ componentName ] ;
512+ if ( Object . keys ( this . overrides [ scopeKey ] [ componentName ] ) . length === 0 ) {
513+ delete this . overrides [ scopeKey ] [ componentName ] ;
514+ }
515+
516+ if ( Object . keys ( this . overrides [ scopeKey ] ) . length === 0 ) {
517+ delete this . overrides [ scopeKey ] ;
424518 }
425519
426520 this . storage . save ( this . overrides ) ;
427521 }
428522
429- private applyVariable ( componentName : string , variable : string , value : string ) : void {
430- const elements = this . elementsByComponent . get ( componentName ) || [ ] ;
523+ private applyVariable ( componentName : string , scopeKey : string , variable : string , value : string ) : void {
524+ const elements = this . getElementsForContext ( componentName , scopeKey ) ;
431525 for ( const element of elements ) {
432526 element . style . setProperty ( variable , value ) ;
433527 }
434528 }
435529
436- private removeVariable ( componentName : string , variable : string ) : void {
437- const elements = this . elementsByComponent . get ( componentName ) || [ ] ;
530+ private removeVariable ( componentName : string , scopeKey : string , variable : string ) : void {
531+ const elements = this . getElementsForContext ( componentName , scopeKey ) ;
438532 for ( const element of elements ) {
439533 element . style . removeProperty ( variable ) ;
440534 }
441535 }
442536
443537 private resetComponent ( componentName : string ) : void {
444- if ( ! confirm ( `Reset all overrides for ${ this . getComponentLabel ( componentName ) } ?` ) ) {
538+ const scopeLabel = this . getScopeLabel ( this . activeScopeKey ) ;
539+ const labelSuffix = scopeLabel ? ` in scope "${ scopeLabel } "` : '' ;
540+ if ( ! confirm ( `Reset all overrides for ${ this . getComponentLabel ( componentName ) } ${ labelSuffix } ?` ) ) {
445541 return ;
446542 }
447543
448- const variables = Object . keys ( this . overrides [ componentName ] || { } ) ;
544+ const variables = Object . keys ( this . overrides [ this . activeScopeKey ] ?. [ componentName ] || { } ) ;
449545 for ( const variable of variables ) {
450- this . removeVariable ( componentName , variable ) ;
546+ this . removeVariable ( componentName , this . activeScopeKey , variable ) ;
451547 }
452548
453- delete this . overrides [ componentName ] ;
549+ if ( this . overrides [ this . activeScopeKey ] ) {
550+ delete this . overrides [ this . activeScopeKey ] [ componentName ] ;
551+ if ( Object . keys ( this . overrides [ this . activeScopeKey ] ) . length === 0 ) {
552+ delete this . overrides [ this . activeScopeKey ] ;
553+ }
554+ }
454555 this . storage . save ( this . overrides ) ;
455556 this . renderControls ( ) ;
456557 }
@@ -460,9 +561,11 @@ class ComponentCustomizationTool {
460561 return ;
461562 }
462563
463- for ( const [ componentName , componentOverrides ] of Object . entries ( this . overrides ) ) {
464- for ( const variable of Object . keys ( componentOverrides ) ) {
465- this . removeVariable ( componentName , variable ) ;
564+ for ( const [ scopeKey , scopeOverrides ] of Object . entries ( this . overrides ) ) {
565+ for ( const [ componentName , componentOverrides ] of Object . entries ( scopeOverrides ) ) {
566+ for ( const variable of Object . keys ( componentOverrides ) ) {
567+ this . removeVariable ( componentName , scopeKey , variable ) ;
568+ }
466569 }
467570 }
468571
0 commit comments