@at-flux/astro-feature-flags 1.0.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/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/dev-toolbar-app.d.mts +16 -0
- package/dist/dev-toolbar-app.d.mts.map +1 -0
- package/dist/dev-toolbar-app.mjs +407 -0
- package/dist/dev-toolbar-app.mjs.map +1 -0
- package/dist/dev-toolbar-flag-icon-BHCQJ53N.mjs +10 -0
- package/dist/dev-toolbar-flag-icon-BHCQJ53N.mjs.map +1 -0
- package/dist/index.d.mts +103 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1107 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runtime.d.mts +115 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +280 -0
- package/dist/runtime.mjs.map +1 -0
- package/docs/assets/element-gating.png +0 -0
- package/docs/assets/page-gating.png +0 -0
- package/docs/assets/toolbar-feature-flags.png +0 -0
- package/docs/how-to/hide-from-sitemaps.md +49 -0
- package/docs/testing.md +40 -0
- package/example/README.md +18 -0
- package/example/astro.config.mjs +26 -0
- package/example/ff.json +26 -0
- package/example/package.json +22 -0
- package/example/pnpm-lock.yaml +4022 -0
- package/example/src/env.d.ts +2 -0
- package/example/src/layouts/Layout.astro +21 -0
- package/example/src/pages/hot/index.astro +23 -0
- package/example/src/pages/hot-dev/index.astro +18 -0
- package/example/src/pages/hot-dev/sub/index.astro +21 -0
- package/example/src/pages/hot-feature-1/index.astro +38 -0
- package/example/src/pages/index.astro +102 -0
- package/example/src/styles/global.css +22 -0
- package/example/tsconfig.json +4 -0
- package/package.json +68 -0
- package/src/badge-layout.ts +146 -0
- package/src/dev-head-inject.ts +26 -0
- package/src/dev-inline-runtimes.ts +397 -0
- package/src/dev-outline-css.ts +449 -0
- package/src/dev-toolbar-app.ts +516 -0
- package/src/dev-toolbar-flag-icon.ts +9 -0
- package/src/index.ts +417 -0
- package/src/inline-script.ts +14 -0
- package/src/production-html-cull.ts +133 -0
- package/src/route-prefix-js.ts +7 -0
- package/src/runtime.ts +575 -0
- package/virtual-astro-feature-flags.d.ts +67 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import {
|
|
2
|
+
elementBadgePositionBlock,
|
|
3
|
+
normalizeElementBadgeLayout,
|
|
4
|
+
} from "./badge-layout";
|
|
5
|
+
import type { ElementBadgeLayoutOptions } from "./badge-layout";
|
|
6
|
+
import { inlineInvoke } from "./inline-script";
|
|
7
|
+
import { affDevBootstrapRuntime } from "./dev-inline-runtimes";
|
|
8
|
+
import type { ResolvedFeatureRuntime } from "./runtime";
|
|
9
|
+
import { toToken } from "./runtime";
|
|
10
|
+
|
|
11
|
+
export type DevOutlineHiddenStrategy = "visibility" | "display";
|
|
12
|
+
|
|
13
|
+
export interface DevOutlineCssOptions extends ElementBadgeLayoutOptions {
|
|
14
|
+
outlineWidth?: string;
|
|
15
|
+
/** Fallback when no per-token colour is set. */
|
|
16
|
+
outlineColor?: string;
|
|
17
|
+
/** Per-flag-token outline / badge / route-badge colour (overrides JSON `colors`). */
|
|
18
|
+
outlineColorByToken?: Record<string, string>;
|
|
19
|
+
outlineOffset?: string;
|
|
20
|
+
borderRadius?: string;
|
|
21
|
+
|
|
22
|
+
/** Default pill text for the `wip` flag token in the dev toolbar. */
|
|
23
|
+
badgeLabelWip?: string;
|
|
24
|
+
/** @deprecated Use {@link badgeLabelWip}. */
|
|
25
|
+
badgeLabelDev?: string;
|
|
26
|
+
badgeLabelByToken?: Record<string, string>;
|
|
27
|
+
|
|
28
|
+
hiddenStrategy?: DevOutlineHiddenStrategy;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @deprecated Unused: badge does not use container queries.
|
|
32
|
+
*/
|
|
33
|
+
badgeMinInlineSize?: string;
|
|
34
|
+
}
|
|
35
|
+
type NormalizedDevOutlineCssOptions = {
|
|
36
|
+
outlineWidth: string;
|
|
37
|
+
outlineColor: string;
|
|
38
|
+
outlineColorByToken: Record<string, string>;
|
|
39
|
+
outlineOffset: string;
|
|
40
|
+
borderRadius: string;
|
|
41
|
+
badgeLabelWip: string;
|
|
42
|
+
badgeLabelByToken: Record<string, string>;
|
|
43
|
+
hiddenStrategy: DevOutlineHiddenStrategy;
|
|
44
|
+
badgeMinInlineSize: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const defaultDevOutlineCssOptions: Omit<
|
|
48
|
+
NormalizedDevOutlineCssOptions,
|
|
49
|
+
"outlineColorByToken"
|
|
50
|
+
> = {
|
|
51
|
+
outlineWidth: "2px",
|
|
52
|
+
outlineColor: "rgb(220 38 38)",
|
|
53
|
+
outlineOffset: "-2px",
|
|
54
|
+
borderRadius: "0.5rem",
|
|
55
|
+
badgeLabelWip: "wip",
|
|
56
|
+
badgeLabelByToken: {},
|
|
57
|
+
hiddenStrategy: "visibility",
|
|
58
|
+
badgeMinInlineSize: "8rem",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function normalizeCssOptions(
|
|
62
|
+
css: DevOutlineCssOptions | undefined,
|
|
63
|
+
runtime: ResolvedFeatureRuntime,
|
|
64
|
+
): NormalizedDevOutlineCssOptions {
|
|
65
|
+
const byToken = {
|
|
66
|
+
...runtime.flagColorsByToken,
|
|
67
|
+
...(css?.outlineColorByToken ?? {}),
|
|
68
|
+
};
|
|
69
|
+
const badgeLabelWip =
|
|
70
|
+
css?.badgeLabelWip ?? css?.badgeLabelDev ?? defaultDevOutlineCssOptions.badgeLabelWip;
|
|
71
|
+
return {
|
|
72
|
+
...defaultDevOutlineCssOptions,
|
|
73
|
+
...(css ?? {}),
|
|
74
|
+
outlineColorByToken: byToken,
|
|
75
|
+
badgeLabelWip,
|
|
76
|
+
badgeLabelByToken: {
|
|
77
|
+
...defaultDevOutlineCssOptions.badgeLabelByToken,
|
|
78
|
+
...(css?.badgeLabelByToken ?? {}),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function colorForToken(
|
|
84
|
+
token: string,
|
|
85
|
+
opts: NormalizedDevOutlineCssOptions,
|
|
86
|
+
): string {
|
|
87
|
+
return opts.outlineColorByToken[token] ?? opts.outlineColor;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** CSS variable per token, set on <html> from ff.json + dev-toolbar overrides. */
|
|
91
|
+
function featureColorVar(token: string, namespace: string): string {
|
|
92
|
+
const ns = toToken(namespace) || "ff";
|
|
93
|
+
return `--${ns}-c-${token}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Dev-only styles: `data-ff="token"` (or space-separated tokens) gates outlines/badges.
|
|
98
|
+
* `html[data-ff-route="<token>"]` (dev) adds a fixed top-right route badge (label only).
|
|
99
|
+
* Toolbar toggles: `data-ff-enabled-*`, `data-ff-outline-*`, `data-ff-badge-*`, `--<namespace>-c-*`.
|
|
100
|
+
*/
|
|
101
|
+
export function createFeatureFlagStyles(
|
|
102
|
+
runtime: ResolvedFeatureRuntime,
|
|
103
|
+
css?: DevOutlineCssOptions,
|
|
104
|
+
): string {
|
|
105
|
+
const nsAttr = `data-${toToken(runtime.namespace) || "ff"}`;
|
|
106
|
+
const selectorsForFlag = (flag: string) => {
|
|
107
|
+
const token = toToken(flag);
|
|
108
|
+
const valueSelectors = [`[${nsAttr}~="${token}"]`];
|
|
109
|
+
if (flag !== token) valueSelectors.push(`[${nsAttr}~="${flag}"]`);
|
|
110
|
+
const baseSelectors = [...valueSelectors, `[${nsAttr}-${token}]`];
|
|
111
|
+
return {
|
|
112
|
+
token,
|
|
113
|
+
baseSelectors,
|
|
114
|
+
selOutline: baseSelectors.map((s) => `${s}:not([${nsAttr}*=" "])`).join(", "),
|
|
115
|
+
selIs: `:is(${baseSelectors.join(", ")})`,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
const opts = normalizeCssOptions(css, runtime);
|
|
119
|
+
const badgeLayout = normalizeElementBadgeLayout(css);
|
|
120
|
+
const badgePos = elementBadgePositionBlock(badgeLayout);
|
|
121
|
+
const chunks: string[] = [];
|
|
122
|
+
|
|
123
|
+
for (const flag of Object.keys(runtime.flags)) {
|
|
124
|
+
const { token, selOutline, selIs } = selectorsForFlag(flag);
|
|
125
|
+
/** Combo hosts use `[${nsAttr}*=" "]` + `data-ff-label`; skip per-token ::before so combo rules win. */
|
|
126
|
+
const selIsSingleBadge = `${selIs}:not([${nsAttr}*=" "])`;
|
|
127
|
+
const col = colorForToken(token, opts);
|
|
128
|
+
const v = featureColorVar(token, runtime.namespace);
|
|
129
|
+
const label =
|
|
130
|
+
opts.badgeLabelByToken[token] ??
|
|
131
|
+
(token === "wip" ? opts.badgeLabelWip : token);
|
|
132
|
+
const labelEscaped = String(label).replace(/'/g, "\\'");
|
|
133
|
+
|
|
134
|
+
chunks.push(`
|
|
135
|
+
html {
|
|
136
|
+
--aff-outline-c-${token}: transparent;
|
|
137
|
+
}
|
|
138
|
+
html:not([data-ff-outline-${token}="off"]) {
|
|
139
|
+
--aff-outline-c-${token}: var(${v}, ${col});
|
|
140
|
+
}
|
|
141
|
+
${selOutline} {
|
|
142
|
+
position: relative;
|
|
143
|
+
outline: ${opts.outlineWidth} solid var(--aff-outline-c-${token});
|
|
144
|
+
outline-offset: ${opts.outlineOffset};
|
|
145
|
+
border-radius: ${opts.borderRadius};
|
|
146
|
+
}
|
|
147
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}::before {
|
|
148
|
+
box-sizing: border-box;
|
|
149
|
+
position: absolute;
|
|
150
|
+
inset: auto;
|
|
151
|
+
margin: 0;
|
|
152
|
+
width: max-content;
|
|
153
|
+
max-width: min(18rem, calc(100% - 0.75rem));
|
|
154
|
+
pointer-events: auto;
|
|
155
|
+
z-index: 20;
|
|
156
|
+
${badgePos}
|
|
157
|
+
display: block;
|
|
158
|
+
content: '${labelEscaped}';
|
|
159
|
+
border: 1px solid color-mix(in oklab, var(${v}, ${col}) 60%, transparent);
|
|
160
|
+
border-radius: 9999px;
|
|
161
|
+
padding: 0.125rem 0.45rem;
|
|
162
|
+
font-size: 0.625rem;
|
|
163
|
+
line-height: 1.2;
|
|
164
|
+
letter-spacing: 0.02em;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
color: var(${v}, ${col});
|
|
167
|
+
background: color-mix(in oklab, white 85%, var(${v}, ${col}) 15%);
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
text-overflow: ellipsis;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
transition: opacity 0.14s ease, background 0.14s ease, border-color 0.14s ease;
|
|
172
|
+
}
|
|
173
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}:hover::before {
|
|
174
|
+
opacity: 0.16;
|
|
175
|
+
background: color-mix(in oklab, white 94%, var(${v}, ${col}) 6%);
|
|
176
|
+
border-color: color-mix(in oklab, var(${v}, ${col}) 35%, transparent);
|
|
177
|
+
}
|
|
178
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-align="start"]::before {
|
|
179
|
+
left: 0.35rem;
|
|
180
|
+
right: auto;
|
|
181
|
+
transform: translateY(calc(-1 * 80%));
|
|
182
|
+
}
|
|
183
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-align="center"]::before {
|
|
184
|
+
left: 50%;
|
|
185
|
+
right: auto;
|
|
186
|
+
transform: translate(-50%, calc(-1 * 80%));
|
|
187
|
+
}
|
|
188
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-align="end"]::before {
|
|
189
|
+
left: auto;
|
|
190
|
+
right: 0.35rem;
|
|
191
|
+
transform: translateY(calc(-1 * 80%));
|
|
192
|
+
}
|
|
193
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-horizontal]::before {
|
|
194
|
+
left: calc(attr(data-ff-horizontal number, 50) * 1%);
|
|
195
|
+
right: auto;
|
|
196
|
+
transform: translate(-50%, calc(-1 * 80%));
|
|
197
|
+
}
|
|
198
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-horizontal][data-ff-anchor="bottom"]::before {
|
|
199
|
+
transform: translate(-50%, 80%);
|
|
200
|
+
}
|
|
201
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-anchor="bottom"]::before {
|
|
202
|
+
top: auto;
|
|
203
|
+
bottom: 0;
|
|
204
|
+
transform: translateY(80%);
|
|
205
|
+
}
|
|
206
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-anchor="bottom"][data-ff-align="center"]::before {
|
|
207
|
+
transform: translate(-50%, 80%);
|
|
208
|
+
}
|
|
209
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-vertical]::before {
|
|
210
|
+
transform: translateY(calc(attr(data-ff-vertical number, 80) * -1%));
|
|
211
|
+
}
|
|
212
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-horizontal][data-ff-vertical]::before {
|
|
213
|
+
transform: translate(-50%, calc(attr(data-ff-vertical number, 80) * -1%));
|
|
214
|
+
}
|
|
215
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-anchor="bottom"][data-ff-vertical]::before {
|
|
216
|
+
transform: translateY(calc(attr(data-ff-vertical number, 80) * 1%));
|
|
217
|
+
}
|
|
218
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-anchor="bottom"][data-ff-horizontal][data-ff-vertical]::before {
|
|
219
|
+
transform: translate(-50%, calc(attr(data-ff-vertical number, 80) * 1%));
|
|
220
|
+
}
|
|
221
|
+
html[data-ff-enabled-${token}="off"] ${selIs} {
|
|
222
|
+
display: none !important;
|
|
223
|
+
}
|
|
224
|
+
`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Multi-token value on a single element: show combined badge text and a gradient outline.
|
|
228
|
+
chunks.push(`
|
|
229
|
+
[${nsAttr}*=" "] {
|
|
230
|
+
position: relative;
|
|
231
|
+
outline: none !important;
|
|
232
|
+
outline-offset: 0 !important;
|
|
233
|
+
border-radius: ${opts.borderRadius};
|
|
234
|
+
}
|
|
235
|
+
html [${nsAttr}*=" "]::after {
|
|
236
|
+
content: "";
|
|
237
|
+
position: absolute;
|
|
238
|
+
inset: 0;
|
|
239
|
+
border-radius: inherit;
|
|
240
|
+
pointer-events: none;
|
|
241
|
+
z-index: 10;
|
|
242
|
+
box-sizing: border-box;
|
|
243
|
+
padding: ${opts.outlineWidth};
|
|
244
|
+
background: var(--ff-combo-gradient, linear-gradient(90deg, #fecaca, #bfdbfe, #bbf7d0));
|
|
245
|
+
-webkit-mask:
|
|
246
|
+
linear-gradient(#000 0 0) content-box,
|
|
247
|
+
linear-gradient(#000 0 0);
|
|
248
|
+
-webkit-mask-composite: xor;
|
|
249
|
+
mask-composite: exclude;
|
|
250
|
+
}
|
|
251
|
+
html [${nsAttr}*=" "]::before {
|
|
252
|
+
box-sizing: border-box;
|
|
253
|
+
position: absolute;
|
|
254
|
+
inset: auto;
|
|
255
|
+
margin: 0;
|
|
256
|
+
width: max-content;
|
|
257
|
+
max-width: min(18rem, calc(100% - 0.75rem));
|
|
258
|
+
pointer-events: auto;
|
|
259
|
+
z-index: 20;
|
|
260
|
+
${badgePos}
|
|
261
|
+
display: block;
|
|
262
|
+
content: attr(${nsAttr});
|
|
263
|
+
background: var(--ff-combo-badge-gradient, var(--ff-combo-gradient-soft, linear-gradient(90deg, #fff1f2, #eff6ff, #f0fdf4))) !important;
|
|
264
|
+
color: var(--ff-combo-text, #111827) !important;
|
|
265
|
+
border: 1px solid var(--ff-combo-badge-border, color-mix(in oklab, #94a3b8 50%, transparent)) !important;
|
|
266
|
+
border-radius: 9999px;
|
|
267
|
+
padding: 0.125rem 0.45rem;
|
|
268
|
+
font-size: 0.625rem;
|
|
269
|
+
line-height: 1.2;
|
|
270
|
+
letter-spacing: 0.02em;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
white-space: nowrap;
|
|
273
|
+
text-overflow: ellipsis;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
transition: opacity 0.14s ease, background 0.14s ease, border-color 0.14s ease;
|
|
276
|
+
}
|
|
277
|
+
html [${nsAttr}*=" "][data-ff-label]::before {
|
|
278
|
+
content: attr(data-ff-label) !important;
|
|
279
|
+
}
|
|
280
|
+
html [${nsAttr}*=" "]:hover::before {
|
|
281
|
+
opacity: 0.2;
|
|
282
|
+
background: linear-gradient(90deg, #fff8f9, #f8fbff, #f8fff9);
|
|
283
|
+
border-color: color-mix(in oklab, #94a3b8 35%, transparent);
|
|
284
|
+
}
|
|
285
|
+
`);
|
|
286
|
+
|
|
287
|
+
// Combo chrome must be suppressed *after* generic combo rules so `!important` / order cannot be
|
|
288
|
+
// overridden by `html [...][data-ff-label]::before { content: ... !important }`.
|
|
289
|
+
for (const flag of Object.keys(runtime.flags)) {
|
|
290
|
+
const { token, selIs } = selectorsForFlag(flag);
|
|
291
|
+
chunks.push(`
|
|
292
|
+
html[data-ff-badge-${token}="off"] ${selIs}[${nsAttr}*=" "]::before,
|
|
293
|
+
html[data-ff-badge-${token}="off"] ${selIs}[${nsAttr}*=" "][data-ff-label]::before {
|
|
294
|
+
display: none !important;
|
|
295
|
+
content: none !important;
|
|
296
|
+
}
|
|
297
|
+
html[data-ff-badge-${token}="off"] ${selIs}[${nsAttr}*=" "]:hover::before,
|
|
298
|
+
html[data-ff-badge-${token}="off"] ${selIs}[${nsAttr}*=" "][data-ff-label]:hover::before {
|
|
299
|
+
display: none !important;
|
|
300
|
+
opacity: 1 !important;
|
|
301
|
+
background: none !important;
|
|
302
|
+
border-color: transparent !important;
|
|
303
|
+
}
|
|
304
|
+
html[data-ff-outline-${token}="off"] ${selIs}[${nsAttr}*=" "] {
|
|
305
|
+
outline: none !important;
|
|
306
|
+
outline-offset: 0 !important;
|
|
307
|
+
}
|
|
308
|
+
html[data-ff-outline-${token}="off"] ${selIs}[${nsAttr}*=" "]::after {
|
|
309
|
+
display: none !important;
|
|
310
|
+
}
|
|
311
|
+
`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
chunks.push(`
|
|
315
|
+
.aff-route-frame {
|
|
316
|
+
position: fixed;
|
|
317
|
+
inset: 0.6rem;
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
z-index: 10040;
|
|
320
|
+
box-sizing: border-box;
|
|
321
|
+
border-radius: 0.5rem;
|
|
322
|
+
overflow: hidden;
|
|
323
|
+
}
|
|
324
|
+
.aff-route-frame::before {
|
|
325
|
+
content: "";
|
|
326
|
+
position: absolute;
|
|
327
|
+
inset: 0;
|
|
328
|
+
border-radius: inherit;
|
|
329
|
+
padding: 2px;
|
|
330
|
+
background: var(--ff-route-outline-gradient, linear-gradient(90deg, #ef4444, #3b82f6, #22c55e));
|
|
331
|
+
-webkit-mask:
|
|
332
|
+
linear-gradient(#000 0 0) content-box,
|
|
333
|
+
linear-gradient(#000 0 0);
|
|
334
|
+
-webkit-mask-composite: xor;
|
|
335
|
+
mask-composite: exclude;
|
|
336
|
+
}
|
|
337
|
+
.aff-route-badge {
|
|
338
|
+
position: fixed;
|
|
339
|
+
top: 0.75rem;
|
|
340
|
+
right: 0.75rem;
|
|
341
|
+
z-index: 10060;
|
|
342
|
+
pointer-events: auto;
|
|
343
|
+
box-sizing: border-box;
|
|
344
|
+
display: inline-flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
max-width: min(34rem, calc(100vw - 1.5rem));
|
|
347
|
+
white-space: nowrap;
|
|
348
|
+
overflow: hidden;
|
|
349
|
+
text-overflow: ellipsis;
|
|
350
|
+
border: 1px solid var(--ff-route-badge-border, color-mix(in oklab, #94a3b8 55%, transparent));
|
|
351
|
+
border-radius: 9999px;
|
|
352
|
+
padding: 0.15rem 0.5rem;
|
|
353
|
+
font-size: 0.625rem;
|
|
354
|
+
line-height: 1.2;
|
|
355
|
+
letter-spacing: 0.02em;
|
|
356
|
+
font-weight: 600;
|
|
357
|
+
color: var(--ff-route-text, #111827);
|
|
358
|
+
background: var(--ff-route-badge-gradient, linear-gradient(90deg, #fff1f2));
|
|
359
|
+
opacity: 1;
|
|
360
|
+
transition: opacity 0.14s ease, border-color 0.14s ease, background 0.14s ease;
|
|
361
|
+
}
|
|
362
|
+
.aff-route-badge:hover {
|
|
363
|
+
opacity: 0.2;
|
|
364
|
+
border-color: color-mix(in oklab, #94a3b8 35%, transparent);
|
|
365
|
+
}
|
|
366
|
+
.aff-route-disabled-overlay {
|
|
367
|
+
position: fixed;
|
|
368
|
+
inset: 0;
|
|
369
|
+
z-index: 10050;
|
|
370
|
+
box-sizing: border-box;
|
|
371
|
+
display: flex;
|
|
372
|
+
align-items: center;
|
|
373
|
+
justify-content: center;
|
|
374
|
+
padding: clamp(1rem, 4vw, 2.5rem);
|
|
375
|
+
text-align: center;
|
|
376
|
+
font: 600 0.95rem/1.45 system-ui, -apple-system, Segoe UI, sans-serif;
|
|
377
|
+
color: #f8fafc;
|
|
378
|
+
background: rgba(15, 23, 42, 0.52);
|
|
379
|
+
backdrop-filter: blur(2px);
|
|
380
|
+
pointer-events: auto;
|
|
381
|
+
}
|
|
382
|
+
.aff-route-disabled-overlay > p {
|
|
383
|
+
margin: 0;
|
|
384
|
+
max-width: min(44rem, calc(100vw - 2rem));
|
|
385
|
+
white-space: pre-line;
|
|
386
|
+
}
|
|
387
|
+
`);
|
|
388
|
+
|
|
389
|
+
for (const flag of Object.keys(runtime.flags)) {
|
|
390
|
+
const token = toToken(flag);
|
|
391
|
+
chunks.push(`
|
|
392
|
+
[data-ff-route~="${token}"][data-ff-outline-${token}="off"] #aff-route-frame {
|
|
393
|
+
display: none !important;
|
|
394
|
+
}
|
|
395
|
+
[data-ff-route~="${token}"][data-ff-badge-${token}="off"] #aff-route-badge {
|
|
396
|
+
display: none !important;
|
|
397
|
+
}
|
|
398
|
+
[data-ff-route~="${token}"][data-ff-enabled-${token}="off"] #aff-route-frame {
|
|
399
|
+
display: none !important;
|
|
400
|
+
}
|
|
401
|
+
[data-ff-route~="${token}"][data-ff-enabled-${token}="off"] #aff-route-badge {
|
|
402
|
+
display: none !important;
|
|
403
|
+
}
|
|
404
|
+
`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return "\n" + chunks.join("\n") + "\n";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* CSS-only hiding for disabled flags. The Astro integration no longer injects this into
|
|
412
|
+
* production builds — static HTML is culled instead. Kept for custom tooling or tests.
|
|
413
|
+
*/
|
|
414
|
+
export function createProductionGateStyles(
|
|
415
|
+
runtime: ResolvedFeatureRuntime,
|
|
416
|
+
): string {
|
|
417
|
+
const nsAttr = `data-${toToken(runtime.namespace) || "ff"}`;
|
|
418
|
+
const chunks: string[] = [];
|
|
419
|
+
for (const [flag, enabled] of Object.entries(runtime.flags)) {
|
|
420
|
+
if (enabled) continue;
|
|
421
|
+
const token = toToken(flag);
|
|
422
|
+
const valueSelectors = [`[${nsAttr}~="${token}"]`];
|
|
423
|
+
if (flag !== token) valueSelectors.push(`[${nsAttr}~="${flag}"]`);
|
|
424
|
+
chunks.push(
|
|
425
|
+
`${[...valueSelectors, `[${nsAttr}-${token}]`].join(", ")} { display: none !important; }`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
return chunks.length ? `\n${chunks.join("\n")}\n` : "";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function buildAffDevBootstrapScript(
|
|
432
|
+
flagTokens: string[],
|
|
433
|
+
colors: Record<string, string>,
|
|
434
|
+
outlineDefaults: Record<string, boolean>,
|
|
435
|
+
badgeDefaults: Record<string, boolean>,
|
|
436
|
+
routeFlags: Record<string, string[]>,
|
|
437
|
+
flagNameToToken: Record<string, string>,
|
|
438
|
+
namespace: string,
|
|
439
|
+
): string {
|
|
440
|
+
return inlineInvoke(affDevBootstrapRuntime, {
|
|
441
|
+
flagTokens,
|
|
442
|
+
colors,
|
|
443
|
+
outlineDefaults,
|
|
444
|
+
badgeDefaults,
|
|
445
|
+
routeFlags,
|
|
446
|
+
flagNameToToken,
|
|
447
|
+
namespace: toToken(namespace) || "ff",
|
|
448
|
+
});
|
|
449
|
+
}
|