fontdue-js 3.0.5 → 3.1.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/CHANGELOG.md +14 -0
- package/dist/__generated__/FeatureTesterCard_fontStyle.graphql.d.ts +23 -0
- package/dist/__generated__/FeatureTesterCard_fontStyle.graphql.js +62 -0
- package/dist/__generated__/FeatureTesterStandaloneQuery.graphql.d.ts +25 -0
- package/dist/__generated__/FeatureTesterStandaloneQuery.graphql.js +240 -0
- package/dist/__generated__/FeatureTestersIdQuery.graphql.d.ts +21 -0
- package/dist/__generated__/FeatureTestersIdQuery.graphql.js +344 -0
- package/dist/__generated__/FeatureTestersSlugQuery.graphql.d.ts +25 -0
- package/dist/__generated__/FeatureTestersSlugQuery.graphql.js +364 -0
- package/dist/__generated__/FeatureTesters_collection.graphql.d.ts +39 -0
- package/dist/__generated__/FeatureTesters_collection.graphql.js +145 -0
- package/dist/__generated__/NodePasswordFormAccessNodeMutation.graphql.d.ts +2 -1
- package/dist/__generated__/NodePasswordFormAccessNodeMutation.graphql.js +10 -4
- package/dist/__generated__/NodePasswordFormElementIDQuery.graphql.d.ts +21 -0
- package/dist/__generated__/NodePasswordFormElementIDQuery.graphql.js +86 -0
- package/dist/__generated__/NodePasswordFormElementSlugQuery.graphql.d.ts +25 -0
- package/dist/__generated__/NodePasswordFormElementSlugQuery.graphql.js +108 -0
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +10 -4
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTesters_collection.graphql.d.ts +2 -1
- package/dist/__generated__/TypeTesters_collection.graphql.js +8 -2
- package/dist/__tests__/createFontdueFetch.test.js +128 -4
- package/dist/__tests__/highlight.test.js +141 -0
- package/dist/__tests__/middleware.test.js +132 -0
- package/dist/components/FeatureTester/FeatureTesterCard.d.ts +30 -0
- package/dist/components/FeatureTester/FeatureTesterCard.js +167 -0
- package/dist/components/FeatureTester/FeatureTesterElement.d.ts +19 -0
- package/dist/components/FeatureTester/FeatureTesterElement.js +42 -0
- package/dist/components/FeatureTester/FeatureTesters.d.ts +43 -0
- package/dist/components/FeatureTester/FeatureTesters.js +156 -0
- package/dist/components/FeatureTester/FeatureTesters.server.d.ts +9 -0
- package/dist/components/FeatureTester/FeatureTesters.server.js +40 -0
- package/dist/components/FeatureTester/FeatureTestersElement.d.ts +10 -0
- package/dist/components/FeatureTester/FeatureTestersElement.js +27 -0
- package/dist/components/FeatureTester/highlight.d.ts +19 -0
- package/dist/components/FeatureTester/highlight.js +71 -0
- package/dist/components/FeatureTester/index.d.ts +26 -0
- package/dist/components/FeatureTester/index.js +66 -0
- package/dist/components/FeatureTester/index.server.d.ts +7 -0
- package/dist/components/FeatureTester/index.server.js +30 -0
- package/dist/components/NodePasswordForm/NodePasswordFormElement.d.ts +3 -0
- package/dist/components/NodePasswordForm/NodePasswordFormElement.js +65 -0
- package/dist/components/NodePasswordForm/index.d.ts +16 -1
- package/dist/components/NodePasswordForm/index.js +30 -59
- package/dist/components/Root/index.js +2 -1
- package/dist/components/TypeTesters/index.js +5 -1
- package/dist/fontdue.css +145 -0
- package/dist/hooks/useFeatureTesterAutofit.d.ts +17 -0
- package/dist/hooks/useFeatureTesterAutofit.js +106 -0
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +16 -1
- package/dist/next/registerSingleTenantResolver.js +13 -1
- package/dist/next/tenant.d.ts +1 -0
- package/dist/next/tenant.js +27 -2
- package/dist/nodeAccess.d.ts +18 -0
- package/dist/nodeAccess.js +84 -0
- package/dist/preview/server.d.ts +1 -20
- package/dist/preview/server.js +16 -85
- package/dist/relay/environment.js +8 -2
- package/dist/relay/serverConfig.d.ts +3 -0
- package/dist/relay/serverConfig.js +22 -1
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.js +90 -7
- package/dist/server/middleware.d.ts +37 -0
- package/dist/server/middleware.js +144 -0
- package/package.json +8 -2
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { sensitiveStringsForFeature, computeSensitivityFlags, segmentByFlags } from "../components/FeatureTester/highlight.js";
|
|
3
|
+
describe('sensitiveStringsForFeature', () => {
|
|
4
|
+
const glyphs = [{
|
|
5
|
+
characters: 'a',
|
|
6
|
+
features: ['ss03']
|
|
7
|
+
}, {
|
|
8
|
+
characters: 'g',
|
|
9
|
+
features: ['ss03', 'ss05']
|
|
10
|
+
}, {
|
|
11
|
+
characters: 'a',
|
|
12
|
+
features: ['ss03']
|
|
13
|
+
},
|
|
14
|
+
// duplicate
|
|
15
|
+
{
|
|
16
|
+
characters: 'x',
|
|
17
|
+
features: null
|
|
18
|
+
},
|
|
19
|
+
// no features
|
|
20
|
+
{
|
|
21
|
+
characters: 'ffi',
|
|
22
|
+
features: ['dlig']
|
|
23
|
+
}];
|
|
24
|
+
it('returns the de-duplicated input strings a feature substitutes', () => {
|
|
25
|
+
expect(sensitiveStringsForFeature(glyphs, 'ss03').sort()).toEqual(['a', 'g']);
|
|
26
|
+
});
|
|
27
|
+
it('matches a feature that is one of several on a glyph', () => {
|
|
28
|
+
expect(sensitiveStringsForFeature(glyphs, 'ss05')).toEqual(['g']);
|
|
29
|
+
});
|
|
30
|
+
it('returns ligature (multi-char) inputs', () => {
|
|
31
|
+
expect(sensitiveStringsForFeature(glyphs, 'dlig')).toEqual(['ffi']);
|
|
32
|
+
});
|
|
33
|
+
it('returns [] for a feature no glyph carries (the contextual signal)', () => {
|
|
34
|
+
expect(sensitiveStringsForFeature(glyphs, 'frac')).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
it('is empty for null/undefined glyphNames', () => {
|
|
37
|
+
expect(sensitiveStringsForFeature(null, 'ss03')).toEqual([]);
|
|
38
|
+
expect(sensitiveStringsForFeature(undefined, 'ss03')).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('computeSensitivityFlags', () => {
|
|
42
|
+
it('flags every occurrence of an auto string (by code point)', () => {
|
|
43
|
+
expect(computeSensitivityFlags('abcabc', {
|
|
44
|
+
autoStrings: ['a']
|
|
45
|
+
})).toEqual([true, false, false, true, false, false]);
|
|
46
|
+
});
|
|
47
|
+
it('flags whole multi-char auto strings', () => {
|
|
48
|
+
expect(computeSensitivityFlags('abcabc', {
|
|
49
|
+
autoStrings: ['ab']
|
|
50
|
+
})).toEqual([true, true, false, true, true, false]);
|
|
51
|
+
});
|
|
52
|
+
it('flags shaped-diff ranges', () => {
|
|
53
|
+
expect(computeSensitivityFlags('abcabc', {
|
|
54
|
+
addMarks: [{
|
|
55
|
+
start: 2,
|
|
56
|
+
end: 4
|
|
57
|
+
}]
|
|
58
|
+
})).toEqual([false, false, true, true, true, false]);
|
|
59
|
+
});
|
|
60
|
+
it('composes ranges on top of auto strings', () => {
|
|
61
|
+
expect(computeSensitivityFlags('abcabc', {
|
|
62
|
+
autoStrings: ['a'],
|
|
63
|
+
addMarks: [{
|
|
64
|
+
start: 2,
|
|
65
|
+
end: 2
|
|
66
|
+
}]
|
|
67
|
+
})).toEqual([true, false, true, true, false, false]);
|
|
68
|
+
});
|
|
69
|
+
it('clamps ranges to the content and ignores negative indices', () => {
|
|
70
|
+
expect(computeSensitivityFlags('abc', {
|
|
71
|
+
addMarks: [{
|
|
72
|
+
start: -2,
|
|
73
|
+
end: 100
|
|
74
|
+
}]
|
|
75
|
+
})).toEqual([true, true, true]);
|
|
76
|
+
});
|
|
77
|
+
it('is code-point aware (astral characters count as one)', () => {
|
|
78
|
+
// '🎉' is one code point; 'a' should be flagged at index 1, not 2
|
|
79
|
+
expect(computeSensitivityFlags('🎉a', {
|
|
80
|
+
autoStrings: ['a']
|
|
81
|
+
})).toEqual([false, true]);
|
|
82
|
+
});
|
|
83
|
+
it('is all-false with no options', () => {
|
|
84
|
+
expect(computeSensitivityFlags('abc', {})).toEqual([false, false, false]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('segmentByFlags', () => {
|
|
88
|
+
it('collapses consecutive runs into segments', () => {
|
|
89
|
+
const flags = [false, false, true, true, false, false];
|
|
90
|
+
expect(segmentByFlags('abcabc', flags)).toEqual([{
|
|
91
|
+
text: 'ab',
|
|
92
|
+
sensitive: false
|
|
93
|
+
}, {
|
|
94
|
+
text: 'ca',
|
|
95
|
+
sensitive: true
|
|
96
|
+
}, {
|
|
97
|
+
text: 'bc',
|
|
98
|
+
sensitive: false
|
|
99
|
+
}]);
|
|
100
|
+
});
|
|
101
|
+
it('keeps a contiguous sensitive run (e.g. a ligature cluster) whole', () => {
|
|
102
|
+
const flags = [false, true, true, true, false, false];
|
|
103
|
+
expect(segmentByFlags('office', flags)).toEqual([{
|
|
104
|
+
text: 'o',
|
|
105
|
+
sensitive: false
|
|
106
|
+
}, {
|
|
107
|
+
text: 'ffi',
|
|
108
|
+
sensitive: true
|
|
109
|
+
}, {
|
|
110
|
+
text: 'ce',
|
|
111
|
+
sensitive: false
|
|
112
|
+
}]);
|
|
113
|
+
});
|
|
114
|
+
it('returns a single segment when nothing is sensitive', () => {
|
|
115
|
+
expect(segmentByFlags('abc', [false, false, false])).toEqual([{
|
|
116
|
+
text: 'abc',
|
|
117
|
+
sensitive: false
|
|
118
|
+
}]);
|
|
119
|
+
});
|
|
120
|
+
it('treats missing flags as not sensitive', () => {
|
|
121
|
+
expect(segmentByFlags('abc', [true])).toEqual([{
|
|
122
|
+
text: 'a',
|
|
123
|
+
sensitive: true
|
|
124
|
+
}, {
|
|
125
|
+
text: 'bc',
|
|
126
|
+
sensitive: false
|
|
127
|
+
}]);
|
|
128
|
+
});
|
|
129
|
+
it('is empty for empty content', () => {
|
|
130
|
+
expect(segmentByFlags('', [])).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
it('is code-point aware', () => {
|
|
133
|
+
expect(segmentByFlags('🎉a', [false, true])).toEqual([{
|
|
134
|
+
text: '🎉',
|
|
135
|
+
sensitive: false
|
|
136
|
+
}, {
|
|
137
|
+
text: 'a',
|
|
138
|
+
sensitive: true
|
|
139
|
+
}]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { runWithPreview, runWithNodeAccess, runWithFontdue, isPreviewing, ambientHeaders } from '../server/middleware.js';
|
|
3
|
+
import { createFontdueFetch } from '../server/index.js';
|
|
4
|
+
import { PREVIEW_TOKEN_COOKIE } from '../preview/constants.js';
|
|
5
|
+
import { NODE_ACCESS_COOKIE, NODE_ACCESS_HEADER } from '../nodeAccess.js';
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.unstubAllGlobals();
|
|
8
|
+
});
|
|
9
|
+
function req() {
|
|
10
|
+
let cookies = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
11
|
+
const parts = [];
|
|
12
|
+
if (cookies.preview) parts.push(`${PREVIEW_TOKEN_COOKIE}=${cookies.preview}`);
|
|
13
|
+
if (cookies.nodeAccess) parts.push(`${NODE_ACCESS_COOKIE}=${cookies.nodeAccess}`);
|
|
14
|
+
return new Request('https://store.test/', {
|
|
15
|
+
headers: parts.length ? {
|
|
16
|
+
cookie: parts.join('; ')
|
|
17
|
+
} : {}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const cdnHeaders = {
|
|
21
|
+
'Netlify-CDN-Cache-Control': 'public, durable, s-maxage=31536000',
|
|
22
|
+
'CDN-Cache-Control': 'public, s-maxage=300'
|
|
23
|
+
};
|
|
24
|
+
describe('runWithNodeAccess', () => {
|
|
25
|
+
it('passes public (no cookie) requests through untouched and cacheable', async () => {
|
|
26
|
+
const response = await runWithNodeAccess(req(), async () => {
|
|
27
|
+
expect(ambientHeaders()).toEqual({});
|
|
28
|
+
return new Response('ok', {
|
|
29
|
+
headers: cdnHeaders
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
expect(response.headers.get('Cache-Control')).toBeNull();
|
|
33
|
+
expect(response.headers.get('Netlify-CDN-Cache-Control')).toBe('public, durable, s-maxage=31536000');
|
|
34
|
+
});
|
|
35
|
+
it('forwards the unlock token ambiently and forces the response out of shared caches', async () => {
|
|
36
|
+
const response = await runWithNodeAccess(req({
|
|
37
|
+
nodeAccess: 'na1'
|
|
38
|
+
}), async () => {
|
|
39
|
+
expect(ambientHeaders()).toEqual({
|
|
40
|
+
[NODE_ACCESS_HEADER]: 'na1'
|
|
41
|
+
});
|
|
42
|
+
// Carrying only a node-access token is not an admin preview.
|
|
43
|
+
expect(isPreviewing()).toBe(false);
|
|
44
|
+
return new Response('secret', {
|
|
45
|
+
headers: cdnHeaders
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
|
|
49
|
+
expect(response.headers.get('Netlify-CDN-Cache-Control')).toBeNull();
|
|
50
|
+
expect(response.headers.get('CDN-Cache-Control')).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('runWithPreview is single-responsibility', () => {
|
|
54
|
+
it('does NOT forward the node-access token (only preview)', async () => {
|
|
55
|
+
const response = await runWithPreview(req({
|
|
56
|
+
nodeAccess: 'na1'
|
|
57
|
+
}), async () => {
|
|
58
|
+
// A node-access cookie alone is invisible to runWithPreview.
|
|
59
|
+
expect(ambientHeaders()).toEqual({});
|
|
60
|
+
expect(isPreviewing()).toBe(false);
|
|
61
|
+
return new Response('ok', {
|
|
62
|
+
headers: cdnHeaders
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
// No preview → public → left cacheable.
|
|
66
|
+
expect(response.headers.get('Cache-Control')).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('runWithFontdue (composed)', () => {
|
|
70
|
+
it('forwards both the preview and node-access tokens together', async () => {
|
|
71
|
+
await runWithFontdue(req({
|
|
72
|
+
preview: 'tok',
|
|
73
|
+
nodeAccess: 'na1'
|
|
74
|
+
}), async () => {
|
|
75
|
+
expect(ambientHeaders()).toEqual({
|
|
76
|
+
authorization: 'Bearer tok',
|
|
77
|
+
[NODE_ACCESS_HEADER]: 'na1'
|
|
78
|
+
});
|
|
79
|
+
expect(isPreviewing()).toBe(true);
|
|
80
|
+
return new Response('ok');
|
|
81
|
+
});
|
|
82
|
+
// Context unwinds after the request.
|
|
83
|
+
expect(ambientHeaders()).toEqual({});
|
|
84
|
+
expect(isPreviewing()).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
it('forwards node-access alone (no preview) and marks the response no-store', async () => {
|
|
87
|
+
const response = await runWithFontdue(req({
|
|
88
|
+
nodeAccess: 'na1'
|
|
89
|
+
}), async () => {
|
|
90
|
+
expect(ambientHeaders()).toEqual({
|
|
91
|
+
[NODE_ACCESS_HEADER]: 'na1'
|
|
92
|
+
});
|
|
93
|
+
expect(isPreviewing()).toBe(false);
|
|
94
|
+
return new Response('ok', {
|
|
95
|
+
headers: cdnHeaders
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
|
|
99
|
+
});
|
|
100
|
+
it('passes fully public requests through untouched and cacheable', async () => {
|
|
101
|
+
const response = await runWithFontdue(req(), async () => {
|
|
102
|
+
expect(ambientHeaders()).toEqual({});
|
|
103
|
+
return new Response('ok', {
|
|
104
|
+
headers: cdnHeaders
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
expect(response.headers.get('Cache-Control')).toBeNull();
|
|
108
|
+
expect(response.headers.get('Netlify-CDN-Cache-Control')).toBe('public, durable, s-maxage=31536000');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('createFontdueFetch + ambient node-access', () => {
|
|
112
|
+
it('a module-level fetcher forwards the ambient unlock token, no per-call plumbing', async () => {
|
|
113
|
+
const fetchMock = vi.fn(async () => ({
|
|
114
|
+
status: 200,
|
|
115
|
+
json: async () => ({
|
|
116
|
+
data: {}
|
|
117
|
+
})
|
|
118
|
+
}));
|
|
119
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
120
|
+
const fetchGraphql = createFontdueFetch({
|
|
121
|
+
url: 'https://acme.fontdue.com'
|
|
122
|
+
});
|
|
123
|
+
await runWithNodeAccess(req({
|
|
124
|
+
nodeAccess: 'na1'
|
|
125
|
+
}), async () => {
|
|
126
|
+
await fetchGraphql('Q', 'query Q { __typename }');
|
|
127
|
+
return new Response('ok');
|
|
128
|
+
});
|
|
129
|
+
const init = fetchMock.mock.calls[0][1];
|
|
130
|
+
expect(init.headers[NODE_ACCESS_HEADER]).toBe('na1');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FeatureTesterCard_fontStyle$key } from '../../__generated__/FeatureTesterCard_fontStyle.graphql.js';
|
|
3
|
+
export type ToggleMode = 'button' | 'hover';
|
|
4
|
+
export interface FeatureTesterCardOptions {
|
|
5
|
+
feature: string;
|
|
6
|
+
content: string;
|
|
7
|
+
label?: string | null;
|
|
8
|
+
fontSize?: number | null;
|
|
9
|
+
lineHeight?: number | null;
|
|
10
|
+
letterSpacing?: number | null;
|
|
11
|
+
alignment?: 'left' | 'center' | 'right' | null;
|
|
12
|
+
defaultOn?: boolean;
|
|
13
|
+
highlightColor?: string | null;
|
|
14
|
+
showLabel?: boolean;
|
|
15
|
+
showTag?: boolean;
|
|
16
|
+
toggle?: ToggleMode;
|
|
17
|
+
autofit?: boolean;
|
|
18
|
+
changedRanges?: ReadonlyArray<{
|
|
19
|
+
readonly start: number;
|
|
20
|
+
readonly length: number;
|
|
21
|
+
}> | null;
|
|
22
|
+
variableSettings?: ReadonlyArray<{
|
|
23
|
+
readonly axis: string;
|
|
24
|
+
readonly value: number;
|
|
25
|
+
}> | null;
|
|
26
|
+
}
|
|
27
|
+
export type FeatureTesterCardProps = FeatureTesterCardOptions & {
|
|
28
|
+
fontStyle: FeatureTesterCard_fontStyle$key;
|
|
29
|
+
};
|
|
30
|
+
export default function FeatureTesterCard({ fontStyle: fontStyleKey, feature, content, label, fontSize, lineHeight, letterSpacing, alignment, defaultOn, highlightColor, showLabel, showTag, toggle, autofit, changedRanges, variableSettings, }: FeatureTesterCardProps): React.JSX.Element;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
4
|
+
import _FeatureTesterCard_fontStyle from "../../__generated__/FeatureTesterCard_fontStyle.graphql.js";
|
|
5
|
+
import React, { useMemo, useState } from 'react';
|
|
6
|
+
import { graphql, useFragment } from 'react-relay';
|
|
7
|
+
import FontStyle from '../FontStyle/index.js';
|
|
8
|
+
import useFeaturesData from '../TypeTester/useFeaturesData.js';
|
|
9
|
+
import useFeatureTesterAutofit from '../../hooks/useFeatureTesterAutofit.js';
|
|
10
|
+
import { sensitiveStringsForFeature, computeSensitivityFlags, segmentByFlags } from './highlight.js';
|
|
11
|
+
const fragment = (_FeatureTesterCard_fontStyle.hash && _FeatureTesterCard_fontStyle.hash !== "687d8afca93d615ed0794f963702d082" && console.error("The definition of 'FeatureTesterCard_fontStyle' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _FeatureTesterCard_fontStyle);
|
|
12
|
+
export default function FeatureTesterCard(_ref) {
|
|
13
|
+
let {
|
|
14
|
+
fontStyle: fontStyleKey,
|
|
15
|
+
feature,
|
|
16
|
+
content,
|
|
17
|
+
label,
|
|
18
|
+
fontSize = 72,
|
|
19
|
+
lineHeight = 1.2,
|
|
20
|
+
letterSpacing = 0,
|
|
21
|
+
alignment,
|
|
22
|
+
defaultOn = true,
|
|
23
|
+
highlightColor,
|
|
24
|
+
showLabel = true,
|
|
25
|
+
showTag = true,
|
|
26
|
+
toggle = 'button',
|
|
27
|
+
autofit = false,
|
|
28
|
+
changedRanges,
|
|
29
|
+
variableSettings
|
|
30
|
+
} = _ref;
|
|
31
|
+
// `data` carries the merged fragment spreads, so the child fragment hooks
|
|
32
|
+
// (FontStyle, useFeaturesData) read their refs off it rather than the raw key.
|
|
33
|
+
const data = useFragment(fragment, fontStyleKey);
|
|
34
|
+
const {
|
|
35
|
+
featureNames
|
|
36
|
+
} = useFeaturesData({
|
|
37
|
+
fontStyle: data
|
|
38
|
+
});
|
|
39
|
+
const [on, setOn] = useState(defaultOn);
|
|
40
|
+
const hover = toggle === 'hover';
|
|
41
|
+
const handleToggle = () => setOn(o => !o);
|
|
42
|
+
|
|
43
|
+
// The server's shaped diff (`changedRanges`) is exact for THIS string —
|
|
44
|
+
// context-free and contextual substitutions alike — so it wins whenever the
|
|
45
|
+
// collection materialized it. The static GSUB map is the fallback for cards
|
|
46
|
+
// with no shaped diff (no OpenType file server-side, or a hand-authored
|
|
47
|
+
// standalone embed); it only covers single + ligature substitutions, and for
|
|
48
|
+
// a mixed feature like frac it can hold just the slash while the real
|
|
49
|
+
// substitution is contextual over the digits — trusting it over the diff
|
|
50
|
+
// would highlight (and per-span apply) the wrong subset.
|
|
51
|
+
const staticStrings = useMemo(() => sensitiveStringsForFeature(data.glyphNames, feature), [data.glyphNames, feature]);
|
|
52
|
+
const contextFree = staticStrings.length > 0;
|
|
53
|
+
const changedMarks = useMemo(() => (changedRanges ?? []).map(r => ({
|
|
54
|
+
start: r.start,
|
|
55
|
+
end: r.start + r.length - 1
|
|
56
|
+
})), [changedRanges]);
|
|
57
|
+
const contextual = changedMarks.length > 0;
|
|
58
|
+
const segments = useMemo(() => segmentByFlags(content,
|
|
59
|
+
// A card with a shaped diff applies the feature container-wide (a
|
|
60
|
+
// per-span apply would sever the context contextual substitutions
|
|
61
|
+
// depend on), so the set that actually changes is exactly the diff —
|
|
62
|
+
// the highlight equals that set. The static-map fallback applies
|
|
63
|
+
// per-span.
|
|
64
|
+
contextual ? computeSensitivityFlags(content, {
|
|
65
|
+
addMarks: changedMarks
|
|
66
|
+
}) : computeSensitivityFlags(content, {
|
|
67
|
+
autoStrings: staticStrings
|
|
68
|
+
})), [contextual, content, staticStrings, changedMarks]);
|
|
69
|
+
const displayLabel = label || featureNames[feature] || feature;
|
|
70
|
+
const featureSetting = `"${feature}" ${on ? 1 : 0}`;
|
|
71
|
+
|
|
72
|
+
// The chosen variable instance's coordinates, as a font-variation-settings
|
|
73
|
+
// value. Applied to the sample and to the autofit measurement so both see
|
|
74
|
+
// the same glyph widths.
|
|
75
|
+
const fontVariationSettings = useMemo(() => variableSettings && variableSettings.length > 0 ? variableSettings.map(s => `"${s.axis}" ${s.value}`).join(', ') : undefined, [variableSettings]);
|
|
76
|
+
|
|
77
|
+
// A contextual feature with no shaped diff and no static entries (a
|
|
78
|
+
// hand-authored standalone embed) has nothing to tint, but the feature must
|
|
79
|
+
// still apply container-wide or the toggle would do nothing.
|
|
80
|
+
const anySensitive = segments.some(seg => seg.sensitive);
|
|
81
|
+
const containerWide = contextual || !contextFree && !anySensitive;
|
|
82
|
+
|
|
83
|
+
// Autofit measures the sample against the card's width in both toggle states
|
|
84
|
+
// and returns a size that fits the wider one; disabled, it returns `fontSize`.
|
|
85
|
+
const {
|
|
86
|
+
ref: rootRef,
|
|
87
|
+
fontSize: renderSize
|
|
88
|
+
} = useFeatureTesterAutofit({
|
|
89
|
+
enabled: autofit,
|
|
90
|
+
text: content,
|
|
91
|
+
fontFamily: `${data.cssFamily} ${data.name}`,
|
|
92
|
+
feature,
|
|
93
|
+
fontSize: fontSize ?? 72,
|
|
94
|
+
letterSpacing: letterSpacing ?? 0,
|
|
95
|
+
fontVariationSettings
|
|
96
|
+
});
|
|
97
|
+
return /*#__PURE__*/React.createElement("div", _extends({
|
|
98
|
+
ref: rootRef,
|
|
99
|
+
className: "fontdue-feature-tester",
|
|
100
|
+
"data-feature": feature,
|
|
101
|
+
"data-on": on,
|
|
102
|
+
"data-toggle": toggle
|
|
103
|
+
// Drives the muted-context styling: only a card with affected glyphs
|
|
104
|
+
// dims its surroundings, so an unmarked hand-authored sample stays at
|
|
105
|
+
// full text colour.
|
|
106
|
+
,
|
|
107
|
+
"data-highlighted": anySensitive
|
|
108
|
+
}, hover ? {
|
|
109
|
+
onMouseEnter: () => setOn(!defaultOn),
|
|
110
|
+
onMouseLeave: () => setOn(defaultOn)
|
|
111
|
+
} : {}), showLabel && /*#__PURE__*/React.createElement("div", {
|
|
112
|
+
className: "fontdue-feature-tester__bar"
|
|
113
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
114
|
+
type: "button",
|
|
115
|
+
className: "fontdue-feature-tester__toggle",
|
|
116
|
+
role: "switch",
|
|
117
|
+
"aria-checked": on,
|
|
118
|
+
"aria-label": displayLabel,
|
|
119
|
+
onClick: handleToggle
|
|
120
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
121
|
+
className: "fontdue-feature-tester__switch",
|
|
122
|
+
"aria-hidden": "true"
|
|
123
|
+
})), /*#__PURE__*/React.createElement("span", {
|
|
124
|
+
className: "fontdue-feature-tester__label",
|
|
125
|
+
onClick: hover ? undefined : handleToggle
|
|
126
|
+
}, displayLabel), showTag && /*#__PURE__*/React.createElement("span", {
|
|
127
|
+
className: "fontdue-feature-tester__tag"
|
|
128
|
+
}, feature)), /*#__PURE__*/React.createElement(FontStyle, {
|
|
129
|
+
Component: "div",
|
|
130
|
+
fontStyle: data,
|
|
131
|
+
className: "fontdue-feature-tester__sample",
|
|
132
|
+
style: {
|
|
133
|
+
fontSize: `${renderSize}px`,
|
|
134
|
+
lineHeight: lineHeight ?? undefined,
|
|
135
|
+
letterSpacing: letterSpacing ? `${letterSpacing}em` : undefined,
|
|
136
|
+
textAlign: alignment ?? undefined,
|
|
137
|
+
fontVariationSettings,
|
|
138
|
+
// Contextual features must shape with full context, so the feature is
|
|
139
|
+
// applied to the whole line. The shaped diff guarantees only the
|
|
140
|
+
// highlighted characters actually change, so the highlight still
|
|
141
|
+
// means exactly "what changes".
|
|
142
|
+
...(containerWide ? {
|
|
143
|
+
fontFeatureSettings: featureSetting
|
|
144
|
+
} : {})
|
|
145
|
+
}
|
|
146
|
+
}, segments.map((seg, i) => seg.sensitive ? /*#__PURE__*/React.createElement("span", {
|
|
147
|
+
key: i,
|
|
148
|
+
className: "fontdue-feature-tester__hit"
|
|
149
|
+
// Context-free features are applied per-highlighted-span, so
|
|
150
|
+
// toggling changes exactly what's tinted (and honours manual
|
|
151
|
+
// subsetting). `font-feature-settings` leaves unlisted (default)
|
|
152
|
+
// features alone, so default ligatures/kerning are preserved. The
|
|
153
|
+
// highlight colour comes from CSS (`--fontdue-feature-tester-
|
|
154
|
+
// highlight`); `highlightColor` forces it inline when set.
|
|
155
|
+
,
|
|
156
|
+
style: {
|
|
157
|
+
...(highlightColor ? {
|
|
158
|
+
color: highlightColor
|
|
159
|
+
} : {}),
|
|
160
|
+
...(containerWide ? {} : {
|
|
161
|
+
fontFeatureSettings: featureSetting
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
}, seg.text) : /*#__PURE__*/React.createElement(React.Fragment, {
|
|
165
|
+
key: i
|
|
166
|
+
}, seg.text))));
|
|
167
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface FeatureTesterElement_props {
|
|
3
|
+
familyName: string;
|
|
4
|
+
styleName: string;
|
|
5
|
+
feature: string;
|
|
6
|
+
content?: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
fontSize?: string;
|
|
9
|
+
lineHeight?: string;
|
|
10
|
+
letterSpacing?: string;
|
|
11
|
+
defaultOn?: string;
|
|
12
|
+
highlightColor?: string;
|
|
13
|
+
showLabel?: string;
|
|
14
|
+
showTag?: string;
|
|
15
|
+
toggle?: string;
|
|
16
|
+
autofit?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const FeatureTesterElement: React.FC<FeatureTesterElement_props>;
|
|
19
|
+
export default FeatureTesterElement;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Deliberately unexposed: this standalone element is not registered in the
|
|
2
|
+
// Root custom-element registry and `fontdue-js/FeatureTester` is not in the
|
|
3
|
+
// package exports. Only the collection-bound `<fontdue-feature-testers>`
|
|
4
|
+
// ships for now; this file is kept for when the standalone is ready.
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import FeatureTester from './index.js';
|
|
7
|
+
const parseBool = (input, fallback) => input === undefined || input === '' ? fallback : input === 'true';
|
|
8
|
+
export const FeatureTesterElement = _ref => {
|
|
9
|
+
let {
|
|
10
|
+
familyName,
|
|
11
|
+
styleName,
|
|
12
|
+
feature,
|
|
13
|
+
content,
|
|
14
|
+
label,
|
|
15
|
+
fontSize,
|
|
16
|
+
lineHeight,
|
|
17
|
+
letterSpacing,
|
|
18
|
+
defaultOn,
|
|
19
|
+
highlightColor,
|
|
20
|
+
showLabel,
|
|
21
|
+
showTag,
|
|
22
|
+
toggle,
|
|
23
|
+
autofit
|
|
24
|
+
} = _ref;
|
|
25
|
+
return /*#__PURE__*/React.createElement(FeatureTester, {
|
|
26
|
+
familyName: familyName,
|
|
27
|
+
styleName: styleName,
|
|
28
|
+
feature: feature,
|
|
29
|
+
content: content || 'Hamburgefonts',
|
|
30
|
+
label: label || undefined,
|
|
31
|
+
fontSize: fontSize ? parseFloat(fontSize) : undefined,
|
|
32
|
+
lineHeight: lineHeight ? parseFloat(lineHeight) : undefined,
|
|
33
|
+
letterSpacing: letterSpacing ? parseFloat(letterSpacing) : undefined,
|
|
34
|
+
defaultOn: parseBool(defaultOn, true),
|
|
35
|
+
highlightColor: highlightColor || undefined,
|
|
36
|
+
showLabel: parseBool(showLabel, true),
|
|
37
|
+
showTag: parseBool(showTag, true),
|
|
38
|
+
toggle: toggle === 'hover' ? 'hover' : 'button',
|
|
39
|
+
autofit: parseBool(autofit, false)
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
export default FeatureTesterElement;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Config } from '../ConfigContext.js';
|
|
3
|
+
import { FeatureTestersIdQuery } from '../../__generated__/FeatureTestersIdQuery.graphql.js';
|
|
4
|
+
import { FeatureTestersSlugQuery } from '../../__generated__/FeatureTestersSlugQuery.graphql.js';
|
|
5
|
+
import { ToggleMode } from './FeatureTesterCard.js';
|
|
6
|
+
import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
|
|
7
|
+
interface ListOptions {
|
|
8
|
+
highlightColor?: string;
|
|
9
|
+
toggle?: ToggleMode;
|
|
10
|
+
autofit?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type FeatureTestersPreloadedQuery = SerializablePreloadedQuery<FeatureTestersIdQuery> | SerializablePreloadedQuery<FeatureTestersSlugQuery>;
|
|
13
|
+
export type LoadFeatureTestersQueryVariables = {
|
|
14
|
+
collectionId: string;
|
|
15
|
+
collectionSlug?: never;
|
|
16
|
+
} | {
|
|
17
|
+
collectionId?: never;
|
|
18
|
+
collectionSlug: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function loadFeatureTestersQuery(variables: LoadFeatureTestersQueryVariables, options?: LoadQueryOptions): Promise<FeatureTestersPreloadedQuery>;
|
|
21
|
+
export declare function FeatureTestersPreloadedIDQueryRenderer({ preloadedQuery, ...options }: {
|
|
22
|
+
preloadedQuery: SerializablePreloadedQuery<FeatureTestersIdQuery>;
|
|
23
|
+
} & ListOptions): React.JSX.Element | null;
|
|
24
|
+
export declare function FeatureTestersPreloadedSlugQueryRenderer({ preloadedQuery, ...options }: {
|
|
25
|
+
preloadedQuery: SerializablePreloadedQuery<FeatureTestersSlugQuery>;
|
|
26
|
+
} & ListOptions): React.JSX.Element | null;
|
|
27
|
+
export type FeatureTestersProps = {
|
|
28
|
+
config?: Config;
|
|
29
|
+
} & ListOptions & ({
|
|
30
|
+
preloadedQuery: FeatureTestersPreloadedQuery;
|
|
31
|
+
collectionId?: never;
|
|
32
|
+
collectionSlug?: never;
|
|
33
|
+
} | {
|
|
34
|
+
preloadedQuery?: never;
|
|
35
|
+
collectionId: string;
|
|
36
|
+
collectionSlug?: never;
|
|
37
|
+
} | {
|
|
38
|
+
preloadedQuery?: never;
|
|
39
|
+
collectionSlug: string;
|
|
40
|
+
collectionId?: never;
|
|
41
|
+
});
|
|
42
|
+
export default function FeatureTesters({ config, ...props }: FeatureTestersProps): React.JSX.Element;
|
|
43
|
+
export {};
|