@dfosco/storyboard-core 1.20.0 → 1.22.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.
- package/package.json +1 -1
- package/src/bodyClasses.js +8 -4
- package/src/featureFlags.js +26 -5
- package/src/index.js +3 -3
- package/src/loader.js +13 -0
- package/src/loader.test.js +33 -1
package/package.json
CHANGED
package/src/bodyClasses.js
CHANGED
|
@@ -11,9 +11,11 @@ import { getAllParams } from './session.js'
|
|
|
11
11
|
import { isHideMode, getAllShadows } from './hideMode.js'
|
|
12
12
|
import { subscribeToHash } from './hashSubscribe.js'
|
|
13
13
|
import { subscribeToStorage } from './localStorage.js'
|
|
14
|
+
import { syncFlagBodyClasses } from './featureFlags.js'
|
|
14
15
|
|
|
15
16
|
const PREFIX = 'sb-'
|
|
16
17
|
const SCENE_PREFIX = 'sb-scene--'
|
|
18
|
+
const FF_PREFIX = 'sb-ff-'
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Sanitize a string for use in a CSS class name.
|
|
@@ -41,13 +43,13 @@ function overrideClass(key, value) {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
|
-
* Get all current sb- classes on body (excluding scene classes).
|
|
46
|
+
* Get all current sb- classes on body (excluding scene and feature-flag classes).
|
|
45
47
|
* @returns {Set<string>}
|
|
46
48
|
*/
|
|
47
49
|
function getCurrentOverrideClasses() {
|
|
48
50
|
const classes = new Set()
|
|
49
51
|
for (const cls of document.body.classList) {
|
|
50
|
-
if (cls.startsWith(PREFIX) && !cls.startsWith(SCENE_PREFIX)) {
|
|
52
|
+
if (cls.startsWith(PREFIX) && !cls.startsWith(SCENE_PREFIX) && !cls.startsWith(FF_PREFIX)) {
|
|
51
53
|
classes.add(cls)
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -108,8 +110,10 @@ export function setSceneClass(name) {
|
|
|
108
110
|
*/
|
|
109
111
|
export function installBodyClassSync() {
|
|
110
112
|
syncOverrideClasses()
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
+
syncFlagBodyClasses()
|
|
114
|
+
const sync = () => { syncOverrideClasses(); syncFlagBodyClasses() }
|
|
115
|
+
const unsubHash = subscribeToHash(sync)
|
|
116
|
+
const unsubStorage = subscribeToStorage(sync)
|
|
113
117
|
return () => {
|
|
114
118
|
unsubHash()
|
|
115
119
|
unsubStorage()
|
package/src/featureFlags.js
CHANGED
|
@@ -15,23 +15,42 @@ import { getParam, setParam, removeParam, getAllParams } from './session.js'
|
|
|
15
15
|
import { getLocal, setLocal, removeLocal, getAllLocal } from './localStorage.js'
|
|
16
16
|
|
|
17
17
|
const FLAG_PREFIX = 'flag.'
|
|
18
|
+
const BODY_CLASS_PREFIX = 'sb-ff-'
|
|
18
19
|
|
|
19
20
|
/** Module-level storage for config defaults */
|
|
20
21
|
let _defaults = {}
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Sync body classes for active feature flags.
|
|
25
|
+
* Adds `sb-ff-{name}` for every flag that resolves to true,
|
|
26
|
+
* removes it for every flag that resolves to false.
|
|
27
|
+
*/
|
|
28
|
+
export function syncFlagBodyClasses() {
|
|
29
|
+
if (typeof document === 'undefined') return
|
|
30
|
+
for (const key of Object.keys(_defaults)) {
|
|
31
|
+
const cls = BODY_CLASS_PREFIX + key
|
|
32
|
+
if (getFlag(key)) {
|
|
33
|
+
document.body.classList.add(cls)
|
|
34
|
+
} else {
|
|
35
|
+
document.body.classList.remove(cls)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
/**
|
|
23
41
|
* Initialize the feature flag system with config defaults.
|
|
24
|
-
*
|
|
42
|
+
* Syncs localStorage with config defaults on every call.
|
|
25
43
|
* @param {Record<string, boolean>} defaults - Flag key → default value
|
|
26
44
|
*/
|
|
27
45
|
export function initFeatureFlags(defaults = {}) {
|
|
28
46
|
_defaults = { ...defaults }
|
|
29
|
-
//
|
|
47
|
+
// Sync localStorage with config defaults — always overwrite so config
|
|
48
|
+
// changes take effect. User overrides live in the URL hash, which is
|
|
49
|
+
// checked first by getFlag(), so this is safe.
|
|
30
50
|
for (const [key, value] of Object.entries(_defaults)) {
|
|
31
|
-
|
|
32
|
-
setLocal(FLAG_PREFIX + key, String(value))
|
|
33
|
-
}
|
|
51
|
+
setLocal(FLAG_PREFIX + key, String(value))
|
|
34
52
|
}
|
|
53
|
+
syncFlagBodyClasses()
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
/**
|
|
@@ -59,6 +78,7 @@ export function getFlag(key) {
|
|
|
59
78
|
*/
|
|
60
79
|
export function setFlag(key, value) {
|
|
61
80
|
setParam(FLAG_PREFIX + key, String(value))
|
|
81
|
+
syncFlagBodyClasses()
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
/**
|
|
@@ -101,6 +121,7 @@ export function resetFlags() {
|
|
|
101
121
|
removeLocal(localKey)
|
|
102
122
|
}
|
|
103
123
|
}
|
|
124
|
+
syncFlagBodyClasses()
|
|
104
125
|
}
|
|
105
126
|
|
|
106
127
|
/**
|
package/src/index.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// Data index initialization
|
|
9
9
|
export { init } from './loader.js'
|
|
10
10
|
|
|
11
|
-
// Scene & record loading
|
|
12
|
-
export { loadScene, listScenes, sceneExists, loadRecord, findRecord, deepMerge } from './loader.js'
|
|
11
|
+
// Scene, object & record loading
|
|
12
|
+
export { loadScene, listScenes, sceneExists, loadRecord, findRecord, loadObject, deepMerge } from './loader.js'
|
|
13
13
|
|
|
14
14
|
// Dot-notation path utilities
|
|
15
15
|
export { getByPath, setByPath, deepClone } from './dotPath.js'
|
|
@@ -38,7 +38,7 @@ export { mountSceneDebug } from './sceneDebug.js'
|
|
|
38
38
|
export { hash, resolveSceneRoute, getSceneMeta } from './viewfinder.js'
|
|
39
39
|
|
|
40
40
|
// Feature flags
|
|
41
|
-
export { initFeatureFlags, getFlag, setFlag, toggleFlag, getAllFlags, resetFlags, getFlagKeys } from './featureFlags.js'
|
|
41
|
+
export { initFeatureFlags, getFlag, setFlag, toggleFlag, getAllFlags, resetFlags, getFlagKeys, syncFlagBodyClasses } from './featureFlags.js'
|
|
42
42
|
|
|
43
43
|
// Plugin configuration
|
|
44
44
|
export { initPlugins, isPluginEnabled, getPluginsConfig } from './plugins.js'
|
package/src/loader.js
CHANGED
|
@@ -209,4 +209,17 @@ export function findRecord(recordName, id) {
|
|
|
209
209
|
return records.find((entry) => entry.id === id) ?? null
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Loads an object data file by name, resolves any nested $ref references,
|
|
214
|
+
* and returns a deep clone.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} objectName - Name of the object file (e.g., "jane-doe")
|
|
217
|
+
* @returns {object|Array} Resolved object data
|
|
218
|
+
*/
|
|
219
|
+
export function loadObject(objectName) {
|
|
220
|
+
const data = loadDataFile(objectName, 'objects')
|
|
221
|
+
const resolved = resolveRefs(structuredClone(data))
|
|
222
|
+
return resolved
|
|
223
|
+
}
|
|
224
|
+
|
|
212
225
|
export { deepMerge }
|
package/src/loader.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { init, loadScene, listScenes, sceneExists, loadRecord, findRecord, deepMerge } from './loader.js'
|
|
1
|
+
import { init, loadScene, listScenes, sceneExists, loadRecord, findRecord, loadObject, deepMerge } from './loader.js'
|
|
2
2
|
|
|
3
3
|
const makeIndex = () => ({
|
|
4
4
|
scenes: {
|
|
@@ -36,6 +36,10 @@ const makeIndex = () => ({
|
|
|
36
36
|
'circular-obj-b': {
|
|
37
37
|
nested: { $ref: 'circular-obj-a' },
|
|
38
38
|
},
|
|
39
|
+
'team-info': {
|
|
40
|
+
team: 'Engineering',
|
|
41
|
+
lead: { $ref: 'jane-doe' },
|
|
42
|
+
},
|
|
39
43
|
},
|
|
40
44
|
records: {
|
|
41
45
|
posts: [
|
|
@@ -243,3 +247,31 @@ describe('deepMerge', () => {
|
|
|
243
247
|
expect(result.c).toBeUndefined()
|
|
244
248
|
})
|
|
245
249
|
})
|
|
250
|
+
|
|
251
|
+
describe('loadObject', () => {
|
|
252
|
+
it('loads object by name', () => {
|
|
253
|
+
const obj = loadObject('jane-doe')
|
|
254
|
+
expect(obj).toEqual({ name: 'Jane Doe', role: 'admin' })
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('resolves $ref within object', () => {
|
|
258
|
+
const obj = loadObject('team-info')
|
|
259
|
+
expect(obj.team).toBe('Engineering')
|
|
260
|
+
expect(obj.lead).toEqual({ name: 'Jane Doe', role: 'admin' })
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('throws for missing object', () => {
|
|
264
|
+
expect(() => loadObject('nonexistent')).toThrow()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('returns deep clone (mutations do not affect index)', () => {
|
|
268
|
+
const obj1 = loadObject('jane-doe')
|
|
269
|
+
obj1.name = 'Modified'
|
|
270
|
+
const obj2 = loadObject('jane-doe')
|
|
271
|
+
expect(obj2.name).toBe('Jane Doe')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('detects circular $ref and throws', () => {
|
|
275
|
+
expect(() => loadObject('circular-obj-a')).toThrow(/circular/i)
|
|
276
|
+
})
|
|
277
|
+
})
|