@hoyrynpm/bsm-kit 0.0.0-stage → 0.1.1

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/README.md CHANGED
@@ -1,3 +1,10 @@
1
- # Temporary Holding Version
1
+ # @hoyrynpm/bsm-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Pure JS helpers shared by Block Styles Manager and Höyry block plugins.
4
+
5
+ Phase 0 ships this package from `packages/bsm-kit` inside the BSM repo via a
6
+ `file:` dependency so builds work before the private npm publish. After Tuki
7
+ publishes `0.1.0` to the `@hoyrynpm` registry, switch BSM to `"@hoyrynpm/bsm-kit": "^0.1.0"`.
8
+
9
+ No `@wordpress/*` dependencies. Runtime settings are read from `window.hoyryBsm`
10
+ at call time.
package/package.json CHANGED
@@ -1,6 +1,27 @@
1
1
  {
2
- "name": "@hoyrynpm/bsm-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@hoyrynpm/bsm-kit",
3
+ "version": "0.1.1",
4
+ "description": "Shared pure helpers for Höyry Block Styles Manager and block plugins",
5
+ "license": "GPL-2.0-or-later",
6
+ "author": "Digi-ja mainostoimisto Höyry",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./config": "./src/config.js",
12
+ "./fluid": "./src/fluid.js",
13
+ "./viewport": "./src/viewport.js",
14
+ "./schemaCss": "./src/schemaCss.js",
15
+ "./stampSchemaVersion": "./src/stampSchemaVersion.js"
16
+ },
17
+ "files": [
18
+ "src",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "test": "node --test test/*.js"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ }
27
+ }
package/src/config.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Runtime settings from window.hoyryBsm (read at call time).
3
+ */
4
+
5
+ export const KIT_API_VERSION = 1;
6
+
7
+ const DEFAULT_ROOT = 16;
8
+ const DEFAULT_FLUID = { min: 320, max: 1920 };
9
+ const DEFAULT_MOBILE_MAX = 800;
10
+ const DEFAULT_ALLOWED = [ 800, 1023 ];
11
+
12
+ /**
13
+ * @return {object|undefined}
14
+ */
15
+ function hoyryBsm() {
16
+ return typeof window !== 'undefined' ? window.hoyryBsm : undefined;
17
+ }
18
+
19
+ /**
20
+ * @return {boolean}
21
+ */
22
+ function apiReady() {
23
+ const raw = hoyryBsm();
24
+ if ( ! raw || typeof raw !== 'object' ) {
25
+ return false;
26
+ }
27
+ const version = Number( raw.apiVersion );
28
+ return Number.isFinite( version ) && version >= KIT_API_VERSION;
29
+ }
30
+
31
+ /**
32
+ * @return {number}
33
+ */
34
+ export function getRootFontSize() {
35
+ const raw = hoyryBsm()?.rootFontSize;
36
+ const n = Number( raw );
37
+ return Number.isFinite( n ) && n > 0 ? n : DEFAULT_ROOT;
38
+ }
39
+
40
+ /**
41
+ * @return {{ min: number, max: number }}
42
+ */
43
+ export function getFluidRange() {
44
+ const raw = hoyryBsm()?.fluidRange;
45
+ const min = Number( raw?.min );
46
+ const max = Number( raw?.max );
47
+ if (
48
+ Number.isFinite( min ) &&
49
+ Number.isFinite( max ) &&
50
+ min > 0 &&
51
+ max > min
52
+ ) {
53
+ return { min, max };
54
+ }
55
+ return { ...DEFAULT_FLUID };
56
+ }
57
+
58
+ /**
59
+ * @return {{ mobileMax: number, allowed: number[], exceptions: Object<string, number> }}
60
+ */
61
+ export function getViewportConfig() {
62
+ if ( ! apiReady() ) {
63
+ return {
64
+ mobileMax: DEFAULT_MOBILE_MAX,
65
+ allowed: [ ...DEFAULT_ALLOWED ],
66
+ exceptions: {},
67
+ };
68
+ }
69
+ const viewports = hoyryBsm()?.viewports || {};
70
+ const mobileMax = Number( viewports.mobileMax );
71
+ const allowed = Array.isArray( viewports.allowed )
72
+ ? viewports.allowed.map( Number ).filter( ( n ) => Number.isFinite( n ) )
73
+ : [ ...DEFAULT_ALLOWED ];
74
+ const rawExceptions = viewports.exceptions;
75
+ const exceptions =
76
+ rawExceptions &&
77
+ typeof rawExceptions === 'object' &&
78
+ ! Array.isArray( rawExceptions )
79
+ ? { ...rawExceptions }
80
+ : {};
81
+ return {
82
+ mobileMax:
83
+ Number.isFinite( mobileMax ) && mobileMax > 0
84
+ ? mobileMax
85
+ : DEFAULT_MOBILE_MAX,
86
+ allowed: allowed.length ? allowed : [ ...DEFAULT_ALLOWED ],
87
+ exceptions,
88
+ };
89
+ }
package/src/fluid.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared fluid clamp helpers. Fallback root 16 and range 320–1920 live only here
3
+ * (and PHP Fluid_Range).
4
+ */
5
+
6
+ import { getFluidRange, getRootFontSize } from './config.js';
7
+
8
+ export { getFluidRange, getRootFontSize } from './config.js';
9
+
10
+ /**
11
+ * Convert a rem quantity to px using the localized root font size.
12
+ *
13
+ * @param {number} rem Rem value.
14
+ * @return {number} Rounded px.
15
+ */
16
+ export function remToPx( rem ) {
17
+ return Math.round( rem * getRootFontSize() );
18
+ }
19
+
20
+ /**
21
+ * Generate clamp() CSS from min/max px, matching Fluid_Range::generate_clamp.
22
+ *
23
+ * @param {number} minValue Smaller px (usually mobile).
24
+ * @param {number} maxValue Larger px (usually desktop).
25
+ * @param {number} [minViewport]
26
+ * @param {number} [maxViewport]
27
+ * @param {number} [rootFontSize]
28
+ * @return {string}
29
+ */
30
+ export function generateClampValue(
31
+ minValue,
32
+ maxValue,
33
+ minViewport,
34
+ maxViewport,
35
+ rootFontSize
36
+ ) {
37
+ const range = getFluidRange();
38
+ const vmin = minViewport ?? range.min;
39
+ const vmax = maxViewport ?? range.max;
40
+ const root = rootFontSize ?? getRootFontSize();
41
+
42
+ const minRem = Math.round( minValue * 1000 ) / ( root * 1000 );
43
+ if ( Math.abs( minValue - maxValue ) < 0.0001 ) {
44
+ return `${ minRem }rem`;
45
+ }
46
+
47
+ const slope =
48
+ Math.round( ( maxValue - minValue ) * 100 * 1000 ) /
49
+ ( ( vmax - vmin ) * 1000 );
50
+ const yAxisIntersection =
51
+ Math.round( ( minValue - vmin * ( slope / 100 ) ) * 1000 ) /
52
+ ( root * 1000 );
53
+ const maxRem = Math.round( maxValue * 1000 ) / ( root * 1000 );
54
+
55
+ return `clamp(${ minRem }rem, ${ yAxisIntersection }rem + ${ slope }vw, ${ maxRem }rem)`;
56
+ }
package/src/index.js ADDED
@@ -0,0 +1,24 @@
1
+ export { KIT_API_VERSION, getRootFontSize, getFluidRange, getViewportConfig } from './config.js';
2
+ export { remToPx, generateClampValue } from './fluid.js';
3
+ export {
4
+ VIEWPORT_DESKTOP,
5
+ VIEWPORT_TABLET,
6
+ VIEWPORT_MOBILE,
7
+ VIEWPORT_MOBILE_MAX,
8
+ SUPPORTS_TABLET_VIEWPORT,
9
+ mobileKey,
10
+ tabletKey,
11
+ declaresTabletViewport,
12
+ isEmptyViewportValue,
13
+ getViewportValue,
14
+ viewportNumberValue,
15
+ isMobileOverride,
16
+ isTabletOverride,
17
+ isMvpOverrideKey,
18
+ writeViewportKey,
19
+ getViewportBreakpoints,
20
+ emitMobileCssVar,
21
+ emitTabletCssVar,
22
+ } from './viewport.js';
23
+ export { resolveCssValue, cssVarName } from './schemaCss.js';
24
+ export { stampSchemaVersion } from './stampSchemaVersion.js';
@@ -0,0 +1,78 @@
1
+ /**
2
+ * JS pair of Schema_Css_Generator declarative css key rules.
3
+ */
4
+
5
+ import { isEmptyViewportValue } from './viewport.js';
6
+
7
+ /**
8
+ * Resolve a field value: stored → fallbackFrom → default.
9
+ *
10
+ * @param {Object|null|undefined} settings styleSettings root or group bag.
11
+ * @param {Object} css Schema css key object.
12
+ * @param {string} [path] Dotted path into settings for the field.
13
+ * @return {*}
14
+ */
15
+ export function resolveCssValue( settings, css, path = '' ) {
16
+ const stored = path ? getByPath( settings, path ) : undefined;
17
+ if ( ! isEmptyViewportValue( stored ) ) {
18
+ return stored;
19
+ }
20
+
21
+ const fallbacks = css?.fallbackFrom;
22
+ const list = Array.isArray( fallbacks )
23
+ ? fallbacks
24
+ : fallbacks
25
+ ? [ fallbacks ]
26
+ : [];
27
+ for ( const fb of list ) {
28
+ const value = getByPath( settings, String( fb ) );
29
+ if ( ! isEmptyViewportValue( value ) ) {
30
+ return value;
31
+ }
32
+ }
33
+
34
+ if ( css && Object.prototype.hasOwnProperty.call( css, 'default' ) ) {
35
+ return css.default;
36
+ }
37
+ return undefined;
38
+ }
39
+
40
+ /**
41
+ * Build CSS custom property name.
42
+ *
43
+ * @param {string} prefix cssVarPrefix.
44
+ * @param {string} varName css.var.
45
+ * @param {string} [state] hover|active.
46
+ * @param {boolean} [mobile]
47
+ * @return {string} --prefix-var[-state][-mobile]
48
+ */
49
+ export function cssVarName( prefix, varName, state = '', mobile = false ) {
50
+ let name = `--${ prefix }-${ varName }`;
51
+ if ( state ) {
52
+ name += `-${ state }`;
53
+ }
54
+ if ( mobile ) {
55
+ name += '-mobile';
56
+ }
57
+ return name;
58
+ }
59
+
60
+ /**
61
+ * @param {Object|null|undefined} root
62
+ * @param {string} path
63
+ * @return {*}
64
+ */
65
+ function getByPath( root, path ) {
66
+ if ( ! root || typeof root !== 'object' || ! path ) {
67
+ return undefined;
68
+ }
69
+ const parts = String( path ).split( '.' );
70
+ let cur = root;
71
+ for ( const part of parts ) {
72
+ if ( cur === null || cur === undefined || typeof cur !== 'object' ) {
73
+ return undefined;
74
+ }
75
+ cur = cur[ part ];
76
+ }
77
+ return cur;
78
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Stamp styleSchemaVersion on new styles or after migration.
3
+ *
4
+ * @param {Object} attributes Block attributes.
5
+ * @param {Function} setAttributes Gutenberg setAttributes.
6
+ * @param {string} version schemaVersion to stamp.
7
+ */
8
+ export function stampSchemaVersion( attributes, setAttributes, version ) {
9
+ if ( typeof setAttributes !== 'function' || typeof version !== 'string' || version === '' ) {
10
+ return;
11
+ }
12
+
13
+ const current = attributes?.styleSchemaVersion;
14
+ if ( typeof current === 'string' && current !== '' ) {
15
+ return;
16
+ }
17
+
18
+ const settings = attributes?.styleSettings;
19
+ const emptySettings =
20
+ settings === undefined ||
21
+ settings === null ||
22
+ ( typeof settings === 'object' && Object.keys( settings ).length === 0 );
23
+
24
+ if ( ! emptySettings ) {
25
+ return;
26
+ }
27
+
28
+ setAttributes( { styleSchemaVersion: version } );
29
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Desktop / mobile values for block-style settings, with an optional tablet band.
3
+ */
4
+
5
+ import { getViewportConfig } from './config.js';
6
+
7
+ /** Desktop viewport id. */
8
+ export const VIEWPORT_DESKTOP = 'desktop';
9
+
10
+ /** Tablet viewport id. Used only when the block declares tablet keys. */
11
+ export const VIEWPORT_TABLET = 'tablet';
12
+
13
+ /** Mobile viewport id. Only the editor device type `Mobile` maps here. */
14
+ export const VIEWPORT_MOBILE = 'mobile';
15
+
16
+ /**
17
+ * Front-end breakpoint in pixels. Generated CSS does not include the query.
18
+ * Block SCSS uses `@media (max-width: 800px)`.
19
+ */
20
+ export const VIEWPORT_MOBILE_MAX = 800;
21
+
22
+ /** Feature flag kept for existing BSM consumers. */
23
+ export const SUPPORTS_TABLET_VIEWPORT = true;
24
+
25
+ /**
26
+ * @param {string} key Desktop setting key.
27
+ * @return {string}
28
+ */
29
+ export function mobileKey( key ) {
30
+ return `${ key }Mobile`;
31
+ }
32
+
33
+ /**
34
+ * @param {string} key Desktop setting key.
35
+ * @return {string}
36
+ */
37
+ export function tabletKey( key ) {
38
+ return `${ key }Tablet`;
39
+ }
40
+
41
+ /**
42
+ * @param {string[]|undefined|null} keys Tablet allow-list from the block.
43
+ * @return {boolean}
44
+ */
45
+ export function declaresTabletViewport( keys ) {
46
+ return Array.isArray( keys ) && keys.length > 0;
47
+ }
48
+
49
+ /**
50
+ * @param {*} value Stored value.
51
+ * @return {boolean}
52
+ */
53
+ export function isEmptyViewportValue( value ) {
54
+ return (
55
+ value === undefined ||
56
+ value === null ||
57
+ value === '' ||
58
+ isNaNViewportValue( value )
59
+ );
60
+ }
61
+
62
+ /**
63
+ * @param {Object|null|undefined} settings Setting object.
64
+ * @param {string} key Desktop setting key.
65
+ * @param {'desktop'|'tablet'|'mobile'} viewport Active viewport.
66
+ * @return {*}
67
+ */
68
+ export function getViewportValue( settings, key, viewport ) {
69
+ if ( viewport === VIEWPORT_MOBILE ) {
70
+ const mobile = settings?.[ mobileKey( key ) ];
71
+ if ( ! isEmptyViewportValue( mobile ) ) {
72
+ return mobile;
73
+ }
74
+ }
75
+ if ( viewport === VIEWPORT_TABLET ) {
76
+ const tablet = settings?.[ tabletKey( key ) ];
77
+ if ( ! isEmptyViewportValue( tablet ) ) {
78
+ return tablet;
79
+ }
80
+ }
81
+ const value = settings?.[ key ];
82
+ return isNaNViewportValue( value ) ? undefined : value;
83
+ }
84
+
85
+ /**
86
+ * @param {*} value Stored or typed value.
87
+ * @return {number|undefined}
88
+ */
89
+ export function viewportNumberValue( value ) {
90
+ if ( typeof value === 'number' ) {
91
+ return Number.isFinite( value ) ? value : undefined;
92
+ }
93
+ if ( typeof value === 'string' ) {
94
+ const trimmed = value.trim();
95
+ if ( trimmed === '' ) {
96
+ return undefined;
97
+ }
98
+ const parsed = parseFloat( trimmed );
99
+ return Number.isFinite( parsed ) ? parsed : undefined;
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ /**
105
+ * @param {Object|null|undefined} settings Setting object.
106
+ * @param {string} key Desktop setting key.
107
+ * @return {boolean}
108
+ */
109
+ export function isMobileOverride( settings, key ) {
110
+ return ! isEmptyViewportValue( settings?.[ mobileKey( key ) ] );
111
+ }
112
+
113
+ /**
114
+ * @param {Object|null|undefined} settings Setting object.
115
+ * @param {string} key Desktop setting key.
116
+ * @return {boolean}
117
+ */
118
+ export function isTabletOverride( settings, key ) {
119
+ return ! isEmptyViewportValue( settings?.[ tabletKey( key ) ] );
120
+ }
121
+
122
+ /**
123
+ * @param {string} key Desktop setting key.
124
+ * @param {string[]} keys Allow-list supplied by the block.
125
+ * @return {boolean}
126
+ */
127
+ export function isMvpOverrideKey( key, keys ) {
128
+ return Array.isArray( keys ) && keys.includes( key );
129
+ }
130
+
131
+ /**
132
+ * @param {string} key Desktop setting key.
133
+ * @param {'desktop'|'tablet'|'mobile'} viewport Active viewport.
134
+ * @param {string[]} keys Mobile allow-list.
135
+ * @param {string[]|undefined} tabletKeys Tablet allow-list.
136
+ * @return {string|null}
137
+ */
138
+ export function writeViewportKey( key, viewport, keys, tabletKeys ) {
139
+ if ( viewport === VIEWPORT_MOBILE && isMvpOverrideKey( key, keys ) ) {
140
+ return mobileKey( key );
141
+ }
142
+ if ( viewport === VIEWPORT_TABLET ) {
143
+ if ( isMvpOverrideKey( key, tabletKeys ) ) {
144
+ return tabletKey( key );
145
+ }
146
+ return null;
147
+ }
148
+ return key;
149
+ }
150
+
151
+ /**
152
+ * @param {{ tabletMin?: number, tabletMax?: number }} [range]
153
+ * @return {{ mobileMax: number, tabletMin?: number, tabletMax?: number }}
154
+ */
155
+ export function getViewportBreakpoints( range = {} ) {
156
+ const source = range && typeof range === 'object' ? range : {};
157
+ const config = getViewportConfig();
158
+ const breakpoints = {
159
+ mobileMax: config.mobileMax || VIEWPORT_MOBILE_MAX,
160
+ };
161
+ const tabletMin = finiteBreakpoint( source.tabletMin );
162
+ const tabletMax = finiteBreakpoint( source.tabletMax );
163
+ if ( tabletMin !== undefined ) {
164
+ breakpoints.tabletMin = tabletMin;
165
+ }
166
+ if ( tabletMax !== undefined ) {
167
+ breakpoints.tabletMax = tabletMax;
168
+ }
169
+ return breakpoints;
170
+ }
171
+
172
+ /**
173
+ * @param {string} name
174
+ * @param {string} cssType
175
+ * @param {*} desktopVal
176
+ * @param {*} mobileVal
177
+ * @param {Function} [getCssVarFn]
178
+ * @return {string}
179
+ */
180
+ export function emitMobileCssVar(
181
+ name,
182
+ cssType,
183
+ desktopVal,
184
+ mobileVal,
185
+ getCssVarFn
186
+ ) {
187
+ if ( typeof getCssVarFn !== 'function' ) {
188
+ return '';
189
+ }
190
+ return getCssVarFn( `${ name }-mobile`, cssType, mobileVal );
191
+ }
192
+
193
+ /**
194
+ * @param {string} name
195
+ * @param {string} cssType
196
+ * @param {*} desktopVal
197
+ * @param {*} tabletVal
198
+ * @param {Function} [getCssVarFn]
199
+ * @return {string}
200
+ */
201
+ export function emitTabletCssVar(
202
+ name,
203
+ cssType,
204
+ desktopVal,
205
+ tabletVal,
206
+ getCssVarFn
207
+ ) {
208
+ if ( typeof getCssVarFn !== 'function' ) {
209
+ return '';
210
+ }
211
+ return getCssVarFn( `${ name }-tablet`, cssType, tabletVal );
212
+ }
213
+
214
+ /**
215
+ * @param {*} value
216
+ * @return {boolean}
217
+ */
218
+ function isNaNViewportValue( value ) {
219
+ return typeof value === 'number' && Number.isNaN( value );
220
+ }
221
+
222
+ /**
223
+ * @param {*} value
224
+ * @return {number|undefined}
225
+ */
226
+ function finiteBreakpoint( value ) {
227
+ return typeof value === 'number' && Number.isFinite( value )
228
+ ? value
229
+ : undefined;
230
+ }