-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #527 from tokens-studio/css-clamp
add css clamp node
- Loading branch information
Showing
4 changed files
with
200 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@tokens-studio/graph-engine": minor | ||
--- | ||
|
||
Add a new node for css clamp function that outputs your accessible css clamp function for fluid dimensions. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { INodeDefinition, ToInput, ToOutput } from '../../index.js'; | ||
import { Node } from '../../programmatic/node.js'; | ||
import { NumberSchema, StringSchema } from '../../schemas/index.js'; | ||
|
||
export default class NodeDefinition extends Node { | ||
static title = 'CSS Accessible Clamp'; | ||
static type = 'studio.tokens.css.accessibleClamp'; | ||
static description = | ||
'Generates a CSS clamp function for fluid typography that uses rem in addition to ensure accessible values.'; | ||
|
||
declare inputs: ToInput<{ | ||
minSize: number; | ||
maxSize: number; | ||
minViewport: number; | ||
maxViewport: number; | ||
baseFontSize: number; | ||
precision: number; | ||
}>; | ||
declare outputs: ToOutput<{ | ||
value: string; | ||
}>; | ||
|
||
constructor(props: INodeDefinition) { | ||
super(props); | ||
this.addInput('minSize', { | ||
type: { | ||
...NumberSchema, | ||
default: 16, | ||
description: 'Minimum font size in pixels' | ||
} | ||
}); | ||
this.addInput('maxSize', { | ||
type: { | ||
...NumberSchema, | ||
default: 24, | ||
description: 'Maximum font size in pixels' | ||
} | ||
}); | ||
this.addInput('minViewport', { | ||
type: { | ||
...NumberSchema, | ||
default: 320, | ||
description: 'Minimum viewport width in pixels' | ||
} | ||
}); | ||
this.addInput('maxViewport', { | ||
type: { | ||
...NumberSchema, | ||
default: 1920, | ||
description: 'Maximum viewport width in pixels' | ||
} | ||
}); | ||
this.addInput('baseFontSize', { | ||
type: { | ||
...NumberSchema, | ||
default: 16, | ||
description: | ||
'Base font size in pixels (usually browser default of 16px)' | ||
} | ||
}); | ||
this.addInput('precision', { | ||
type: { | ||
...NumberSchema, | ||
default: 3, | ||
minimum: 0, | ||
description: 'Number of decimal places in the output' | ||
} | ||
}); | ||
this.addOutput('value', { | ||
type: StringSchema | ||
}); | ||
} | ||
|
||
execute(): void | Promise<void> { | ||
const { | ||
minSize, | ||
maxSize, | ||
minViewport, | ||
maxViewport, | ||
baseFontSize, | ||
precision | ||
} = this.getAllInputs(); | ||
|
||
// Convert sizes to rem | ||
const minSizeRem = minSize / baseFontSize; | ||
const maxSizeRem = maxSize / baseFontSize; | ||
|
||
// Calculate the preferred value parameters | ||
const slope = (maxSize - minSize) / (maxViewport - minViewport); | ||
const viewportValue = slope * 100; // Convert to vw units | ||
const intersect = minSize - slope * minViewport; | ||
const relativeValue = intersect / baseFontSize; // Convert to rem | ||
|
||
// Format the values with specified precision | ||
const formattedMin = Number(minSizeRem.toFixed(precision)); | ||
const formattedMax = Number(maxSizeRem.toFixed(precision)); | ||
const formattedVw = Number(viewportValue.toFixed(precision)); | ||
const formattedRel = Number(relativeValue.toFixed(precision)); | ||
|
||
// Construct the clamp function | ||
const clampValue = `clamp(${formattedMin}rem, calc(${formattedVw}vw + ${formattedRel}rem), ${formattedMax}rem)`; | ||
|
||
this.outputs.value.set(clampValue); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import accessibleClamp from './accessibleClamp.js'; | ||
import box from './box.js'; | ||
import cssFunction from './cssFunction.js'; | ||
import map from './map.js'; | ||
|
||
export const nodes = [box, cssFunction, map]; | ||
export const nodes = [box, accessibleClamp, cssFunction, map]; |
88 changes: 88 additions & 0 deletions
88
packages/graph-engine/tests/suites/nodes/css/accessibleClamp.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { Graph } from '../../../../src/graph/graph.js'; | ||
import { describe, expect, test } from 'vitest'; | ||
import Node from '../../../../src/nodes/css/accessibleClamp.js'; | ||
|
||
describe('css/clamp', () => { | ||
test('generates clamp function with default values', () => { | ||
const graph = new Graph(); | ||
const node = new Node({ graph }); | ||
|
||
node.execute(); | ||
|
||
expect(node.outputs.value.value).toBe( | ||
'clamp(1rem, calc(0.5vw + 0.9rem), 1.5rem)' | ||
); | ||
}); | ||
|
||
test('generates clamp function with custom values', () => { | ||
const graph = new Graph(); | ||
const node = new Node({ graph }); | ||
|
||
node.inputs.minSize.setValue(16); | ||
node.inputs.maxSize.setValue(32); | ||
node.inputs.minViewport.setValue(320); | ||
node.inputs.maxViewport.setValue(1200); | ||
node.inputs.baseFontSize.setValue(16); | ||
node.inputs.precision.setValue(3); | ||
|
||
node.execute(); | ||
|
||
expect(node.outputs.value.value).toBe( | ||
'clamp(1rem, calc(1.818vw + 0.636rem), 2rem)' | ||
); | ||
}); | ||
|
||
test('handles different precision values', () => { | ||
const graph = new Graph(); | ||
const node = new Node({ graph }); | ||
|
||
node.inputs.minSize.setValue(16); | ||
node.inputs.maxSize.setValue(32); | ||
node.inputs.minViewport.setValue(320); | ||
node.inputs.maxViewport.setValue(1200); | ||
node.inputs.baseFontSize.setValue(16); | ||
node.inputs.precision.setValue(1); | ||
|
||
node.execute(); | ||
|
||
expect(node.outputs.value.value).toBe( | ||
'clamp(1rem, calc(1.8vw + 0.6rem), 2rem)' | ||
); | ||
}); | ||
|
||
test('handles zero precision', () => { | ||
const graph = new Graph(); | ||
const node = new Node({ graph }); | ||
|
||
node.inputs.minSize.setValue(16); | ||
node.inputs.maxSize.setValue(32); | ||
node.inputs.minViewport.setValue(320); | ||
node.inputs.maxViewport.setValue(1200); | ||
node.inputs.baseFontSize.setValue(16); | ||
node.inputs.precision.setValue(0); | ||
|
||
node.execute(); | ||
|
||
expect(node.outputs.value.value).toBe( | ||
'clamp(1rem, calc(2vw + 1rem), 2rem)' | ||
); | ||
}); | ||
|
||
test('handles high precision values', () => { | ||
const graph = new Graph(); | ||
const node = new Node({ graph }); | ||
|
||
node.inputs.minSize.setValue(16); | ||
node.inputs.maxSize.setValue(32); | ||
node.inputs.minViewport.setValue(320); | ||
node.inputs.maxViewport.setValue(1200); | ||
node.inputs.baseFontSize.setValue(16); | ||
node.inputs.precision.setValue(5); | ||
|
||
node.execute(); | ||
|
||
expect(node.outputs.value.value).toBe( | ||
'clamp(1rem, calc(1.81818vw + 0.63636rem), 2rem)' | ||
); | ||
}); | ||
}); |