@mesgflow/ui-kit 0.6.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 +200 -0
- package/fesm2022/mesgflow-ui-kit-accordion.mjs +75 -0
- package/fesm2022/mesgflow-ui-kit-accordion.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-alert.mjs +165 -0
- package/fesm2022/mesgflow-ui-kit-alert.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-avatar-group.mjs +88 -0
- package/fesm2022/mesgflow-ui-kit-avatar-group.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-avatar.mjs +123 -0
- package/fesm2022/mesgflow-ui-kit-avatar.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-badge.mjs +68 -0
- package/fesm2022/mesgflow-ui-kit-badge.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-banner.mjs +111 -0
- package/fesm2022/mesgflow-ui-kit-banner.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-box-input.mjs +78 -0
- package/fesm2022/mesgflow-ui-kit-box-input.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-brand.mjs +120 -0
- package/fesm2022/mesgflow-ui-kit-brand.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-breadcrumb.mjs +84 -0
- package/fesm2022/mesgflow-ui-kit-breadcrumb.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-button-group.mjs +37 -0
- package/fesm2022/mesgflow-ui-kit-button-group.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-button.mjs +359 -0
- package/fesm2022/mesgflow-ui-kit-button.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-card.mjs +128 -0
- package/fesm2022/mesgflow-ui-kit-card.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-chart.mjs +538 -0
- package/fesm2022/mesgflow-ui-kit-chart.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-checkbox.mjs +45 -0
- package/fesm2022/mesgflow-ui-kit-checkbox.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-chip.mjs +123 -0
- package/fesm2022/mesgflow-ui-kit-chip.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-code-block.mjs +71 -0
- package/fesm2022/mesgflow-ui-kit-code-block.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-collapsible.mjs +89 -0
- package/fesm2022/mesgflow-ui-kit-collapsible.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-color-picker.mjs +210 -0
- package/fesm2022/mesgflow-ui-kit-color-picker.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-combobox.mjs +181 -0
- package/fesm2022/mesgflow-ui-kit-combobox.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-command.mjs +491 -0
- package/fesm2022/mesgflow-ui-kit-command.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-context-menu.mjs +92 -0
- package/fesm2022/mesgflow-ui-kit-context-menu.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-copy-button.mjs +167 -0
- package/fesm2022/mesgflow-ui-kit-copy-button.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-core.mjs +624 -0
- package/fesm2022/mesgflow-ui-kit-core.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-date-picker.mjs +643 -0
- package/fesm2022/mesgflow-ui-kit-date-picker.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-description-list.mjs +76 -0
- package/fesm2022/mesgflow-ui-kit-description-list.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-dialog.mjs +700 -0
- package/fesm2022/mesgflow-ui-kit-dialog.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-divider.mjs +62 -0
- package/fesm2022/mesgflow-ui-kit-divider.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-dropzone.mjs +126 -0
- package/fesm2022/mesgflow-ui-kit-dropzone.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-empty-state.mjs +70 -0
- package/fesm2022/mesgflow-ui-kit-empty-state.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-field.mjs +42 -0
- package/fesm2022/mesgflow-ui-kit-field.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-hover-card.mjs +171 -0
- package/fesm2022/mesgflow-ui-kit-hover-card.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-i18n-tr.mjs +258 -0
- package/fesm2022/mesgflow-ui-kit-i18n-tr.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-icon.mjs +174 -0
- package/fesm2022/mesgflow-ui-kit-icon.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-input.mjs +448 -0
- package/fesm2022/mesgflow-ui-kit-input.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-kbd.mjs +24 -0
- package/fesm2022/mesgflow-ui-kit-kbd.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-link.mjs +48 -0
- package/fesm2022/mesgflow-ui-kit-link.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-menu.mjs +105 -0
- package/fesm2022/mesgflow-ui-kit-menu.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-number-input.mjs +153 -0
- package/fesm2022/mesgflow-ui-kit-number-input.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-otp-input.mjs +164 -0
- package/fesm2022/mesgflow-ui-kit-otp-input.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-page-header.mjs +83 -0
- package/fesm2022/mesgflow-ui-kit-page-header.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-pagination.mjs +99 -0
- package/fesm2022/mesgflow-ui-kit-pagination.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-password-input.mjs +120 -0
- package/fesm2022/mesgflow-ui-kit-password-input.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-popover.mjs +213 -0
- package/fesm2022/mesgflow-ui-kit-popover.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-progress.mjs +82 -0
- package/fesm2022/mesgflow-ui-kit-progress.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-radio.mjs +123 -0
- package/fesm2022/mesgflow-ui-kit-radio.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-rating.mjs +116 -0
- package/fesm2022/mesgflow-ui-kit-rating.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-scroll-area.mjs +84 -0
- package/fesm2022/mesgflow-ui-kit-scroll-area.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-search-input.mjs +154 -0
- package/fesm2022/mesgflow-ui-kit-search-input.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-segmented-control.mjs +95 -0
- package/fesm2022/mesgflow-ui-kit-segmented-control.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-select.mjs +227 -0
- package/fesm2022/mesgflow-ui-kit-select.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-sidebar.mjs +449 -0
- package/fesm2022/mesgflow-ui-kit-sidebar.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-skeleton.mjs +74 -0
- package/fesm2022/mesgflow-ui-kit-skeleton.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-slider.mjs +179 -0
- package/fesm2022/mesgflow-ui-kit-slider.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-spinner.mjs +67 -0
- package/fesm2022/mesgflow-ui-kit-spinner.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-splitter.mjs +97 -0
- package/fesm2022/mesgflow-ui-kit-splitter.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-stat-tile.mjs +98 -0
- package/fesm2022/mesgflow-ui-kit-stat-tile.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-stepper.mjs +133 -0
- package/fesm2022/mesgflow-ui-kit-stepper.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-switch.mjs +73 -0
- package/fesm2022/mesgflow-ui-kit-switch.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-table.mjs +1783 -0
- package/fesm2022/mesgflow-ui-kit-table.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-tabs.mjs +569 -0
- package/fesm2022/mesgflow-ui-kit-tabs.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-tag-input.mjs +138 -0
- package/fesm2022/mesgflow-ui-kit-tag-input.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-theme-switcher.mjs +53 -0
- package/fesm2022/mesgflow-ui-kit-theme-switcher.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-theming.mjs +1016 -0
- package/fesm2022/mesgflow-ui-kit-theming.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-time-picker.mjs +138 -0
- package/fesm2022/mesgflow-ui-kit-time-picker.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-timeline.mjs +91 -0
- package/fesm2022/mesgflow-ui-kit-timeline.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-toast.mjs +202 -0
- package/fesm2022/mesgflow-ui-kit-toast.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-toggle.mjs +243 -0
- package/fesm2022/mesgflow-ui-kit-toggle.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-toolbar.mjs +99 -0
- package/fesm2022/mesgflow-ui-kit-toolbar.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-tooltip.mjs +160 -0
- package/fesm2022/mesgflow-ui-kit-tooltip.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-tour.mjs +272 -0
- package/fesm2022/mesgflow-ui-kit-tour.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-tree-select.mjs +177 -0
- package/fesm2022/mesgflow-ui-kit-tree-select.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit-tree.mjs +82 -0
- package/fesm2022/mesgflow-ui-kit-tree.mjs.map +1 -0
- package/fesm2022/mesgflow-ui-kit.mjs +12851 -0
- package/fesm2022/mesgflow-ui-kit.mjs.map +1 -0
- package/package.json +374 -0
- package/preset.css +20 -0
- package/tokens/color.css +693 -0
- package/tokens/contrast.css +68 -0
- package/tokens/elevation.css +68 -0
- package/tokens/motion.css +277 -0
- package/tokens/radius.css +69 -0
- package/tokens/typography.css +127 -0
- package/tokens.css +41 -0
- package/types/mesgflow-ui-kit-accordion.d.ts +42 -0
- package/types/mesgflow-ui-kit-alert.d.ts +60 -0
- package/types/mesgflow-ui-kit-avatar-group.d.ts +28 -0
- package/types/mesgflow-ui-kit-avatar.d.ts +23 -0
- package/types/mesgflow-ui-kit-badge.d.ts +39 -0
- package/types/mesgflow-ui-kit-banner.d.ts +42 -0
- package/types/mesgflow-ui-kit-box-input.d.ts +45 -0
- package/types/mesgflow-ui-kit-brand.d.ts +48 -0
- package/types/mesgflow-ui-kit-breadcrumb.d.ts +26 -0
- package/types/mesgflow-ui-kit-button-group.d.ts +17 -0
- package/types/mesgflow-ui-kit-button.d.ts +75 -0
- package/types/mesgflow-ui-kit-card.d.ts +71 -0
- package/types/mesgflow-ui-kit-chart.d.ts +242 -0
- package/types/mesgflow-ui-kit-checkbox.d.ts +17 -0
- package/types/mesgflow-ui-kit-chip.d.ts +46 -0
- package/types/mesgflow-ui-kit-code-block.d.ts +56 -0
- package/types/mesgflow-ui-kit-collapsible.d.ts +39 -0
- package/types/mesgflow-ui-kit-color-picker.d.ts +78 -0
- package/types/mesgflow-ui-kit-combobox.d.ts +74 -0
- package/types/mesgflow-ui-kit-command.d.ts +185 -0
- package/types/mesgflow-ui-kit-context-menu.d.ts +40 -0
- package/types/mesgflow-ui-kit-copy-button.d.ts +47 -0
- package/types/mesgflow-ui-kit-core.d.ts +508 -0
- package/types/mesgflow-ui-kit-date-picker.d.ts +215 -0
- package/types/mesgflow-ui-kit-description-list.d.ts +23 -0
- package/types/mesgflow-ui-kit-dialog.d.ts +342 -0
- package/types/mesgflow-ui-kit-divider.d.ts +23 -0
- package/types/mesgflow-ui-kit-dropzone.d.ts +47 -0
- package/types/mesgflow-ui-kit-empty-state.d.ts +18 -0
- package/types/mesgflow-ui-kit-field.d.ts +26 -0
- package/types/mesgflow-ui-kit-hover-card.d.ts +52 -0
- package/types/mesgflow-ui-kit-i18n-tr.d.ts +14 -0
- package/types/mesgflow-ui-kit-icon.d.ts +76 -0
- package/types/mesgflow-ui-kit-input.d.ts +186 -0
- package/types/mesgflow-ui-kit-kbd.d.ts +9 -0
- package/types/mesgflow-ui-kit-link.d.ts +20 -0
- package/types/mesgflow-ui-kit-menu.d.ts +53 -0
- package/types/mesgflow-ui-kit-number-input.d.ts +55 -0
- package/types/mesgflow-ui-kit-otp-input.d.ts +44 -0
- package/types/mesgflow-ui-kit-page-header.d.ts +31 -0
- package/types/mesgflow-ui-kit-pagination.d.ts +50 -0
- package/types/mesgflow-ui-kit-password-input.d.ts +33 -0
- package/types/mesgflow-ui-kit-popover.d.ts +62 -0
- package/types/mesgflow-ui-kit-progress.d.ts +32 -0
- package/types/mesgflow-ui-kit-radio.d.ts +59 -0
- package/types/mesgflow-ui-kit-rating.d.ts +56 -0
- package/types/mesgflow-ui-kit-scroll-area.d.ts +27 -0
- package/types/mesgflow-ui-kit-search-input.d.ts +72 -0
- package/types/mesgflow-ui-kit-segmented-control.d.ts +54 -0
- package/types/mesgflow-ui-kit-select.d.ts +89 -0
- package/types/mesgflow-ui-kit-sidebar.d.ts +224 -0
- package/types/mesgflow-ui-kit-skeleton.d.ts +29 -0
- package/types/mesgflow-ui-kit-slider.d.ts +40 -0
- package/types/mesgflow-ui-kit-spinner.d.ts +31 -0
- package/types/mesgflow-ui-kit-splitter.d.ts +40 -0
- package/types/mesgflow-ui-kit-stat-tile.d.ts +54 -0
- package/types/mesgflow-ui-kit-stepper.d.ts +48 -0
- package/types/mesgflow-ui-kit-switch.d.ts +34 -0
- package/types/mesgflow-ui-kit-table.d.ts +805 -0
- package/types/mesgflow-ui-kit-tabs.d.ts +269 -0
- package/types/mesgflow-ui-kit-tag-input.d.ts +33 -0
- package/types/mesgflow-ui-kit-theme-switcher.d.ts +27 -0
- package/types/mesgflow-ui-kit-theming.d.ts +745 -0
- package/types/mesgflow-ui-kit-time-picker.d.ts +54 -0
- package/types/mesgflow-ui-kit-timeline.d.ts +27 -0
- package/types/mesgflow-ui-kit-toast.d.ts +94 -0
- package/types/mesgflow-ui-kit-toggle.d.ts +110 -0
- package/types/mesgflow-ui-kit-toolbar.d.ts +52 -0
- package/types/mesgflow-ui-kit-tooltip.d.ts +55 -0
- package/types/mesgflow-ui-kit-tour.d.ts +66 -0
- package/types/mesgflow-ui-kit-tree-select.d.ts +65 -0
- package/types/mesgflow-ui-kit-tree.d.ts +44 -0
- package/types/mesgflow-ui-kit.d.ts +5444 -0
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, DOCUMENT, signal, computed, DestroyRef, effect, Service } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* mesgflow — OKLCH renk matematigi (Bjorn Ottosson formulleri,
|
|
6
|
+
* https://bottosson.github.io/posts/oklab/). BAGIMSIZ (culori vb. bir
|
|
7
|
+
* kutuphane YOK, bkz. plan Faz 2) — yalnizca `theming/ramp.ts`'in ihtiyaci
|
|
8
|
+
* kadar: hex <-> OKLCH donusumu + basit gamut-kirpma. Bu dosya PUBLIC API
|
|
9
|
+
* DEGIL, `theming/index.ts` bunu re-export ETMEZ.
|
|
10
|
+
*/
|
|
11
|
+
function srgbToLinear(c8) {
|
|
12
|
+
const v = c8 / 255;
|
|
13
|
+
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
14
|
+
}
|
|
15
|
+
function linearToSrgb8(v) {
|
|
16
|
+
const clamped = Math.min(1, Math.max(0, v));
|
|
17
|
+
const c = clamped <= 0.0031308 ? clamped * 12.92 : 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
|
|
18
|
+
return Math.round(c * 255);
|
|
19
|
+
}
|
|
20
|
+
function linearRgbToOklab(r, g, b) {
|
|
21
|
+
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
22
|
+
const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
23
|
+
const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
24
|
+
const l_ = Math.cbrt(l);
|
|
25
|
+
const m_ = Math.cbrt(m);
|
|
26
|
+
const s_ = Math.cbrt(s);
|
|
27
|
+
return [
|
|
28
|
+
0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
|
|
29
|
+
1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
|
|
30
|
+
0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
function oklabToLinearRgb(L, a, b) {
|
|
34
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
35
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
36
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
37
|
+
const l = l_ * l_ * l_;
|
|
38
|
+
const m = m_ * m_ * m_;
|
|
39
|
+
const s = s_ * s_ * s_;
|
|
40
|
+
return [
|
|
41
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
42
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
43
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
/** `#rgb`/`#rrggbb` -> `[r,g,b]` (0-255). Gecersiz girdi icin `[0,0,0]` (cagiran `parseHex` ile once dogrular). */
|
|
47
|
+
function hexToRgb(hex) {
|
|
48
|
+
const clean = hex.trim().replace('#', '');
|
|
49
|
+
const full = clean.length === 3
|
|
50
|
+
? clean
|
|
51
|
+
.split('')
|
|
52
|
+
.map((c) => c + c)
|
|
53
|
+
.join('')
|
|
54
|
+
: clean;
|
|
55
|
+
const num = parseInt(full, 16);
|
|
56
|
+
if (Number.isNaN(num))
|
|
57
|
+
return [0, 0, 0];
|
|
58
|
+
return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
|
|
59
|
+
}
|
|
60
|
+
function rgbToHex(r, g, b) {
|
|
61
|
+
const toHex = (n) => Math.round(Math.min(255, Math.max(0, n)))
|
|
62
|
+
.toString(16)
|
|
63
|
+
.padStart(2, '0');
|
|
64
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
65
|
+
}
|
|
66
|
+
/** `/^#([0-9a-f]{3}|[0-9a-f]{6})$/i` — `createAccentRamp`/`applyMfTheme`'in girdi dogrulamasi icin. */
|
|
67
|
+
function isValidHex(hex) {
|
|
68
|
+
return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex.trim());
|
|
69
|
+
}
|
|
70
|
+
function hexToOklch(hex) {
|
|
71
|
+
const [r8, g8, b8] = hexToRgb(hex);
|
|
72
|
+
const r = srgbToLinear(r8);
|
|
73
|
+
const g = srgbToLinear(g8);
|
|
74
|
+
const b = srgbToLinear(b8);
|
|
75
|
+
const [L, a, bLab] = linearRgbToOklab(r, g, b);
|
|
76
|
+
const c = Math.sqrt(a * a + bLab * bLab);
|
|
77
|
+
let h = (Math.atan2(bLab, a) * 180) / Math.PI;
|
|
78
|
+
if (h < 0)
|
|
79
|
+
h += 360;
|
|
80
|
+
return { l: L, c, h };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* OKLCH -> hex, basit gamut-kirpma ile: sRGB disina tasan L/C/H kombinasyonlari
|
|
84
|
+
* icin kroma kademeli olarak (%92 adimlarla) azaltilir ta ki her kanal [0,1]
|
|
85
|
+
* araligina girene kadar (en fazla 24 deneme — pratikte 5-6 yeter). Tam bir
|
|
86
|
+
* gamut-mapping algoritmasi DEGIL (ör. CSS Color 4'un "CSS Gamut Mapping"i)
|
|
87
|
+
* ama bir tema uretici icin yeterli: sonuc HER ZAMAN gecerli bir sRGB hex,
|
|
88
|
+
* ton hafifce kayabilir (asiri doygun girdilerde) ama asla clamp-artefact'i
|
|
89
|
+
* (yanlis renk) uretmez.
|
|
90
|
+
*/
|
|
91
|
+
function oklchToHex(l, c, h) {
|
|
92
|
+
const hRad = (h * Math.PI) / 180;
|
|
93
|
+
let chroma = c;
|
|
94
|
+
for (let i = 0; i < 24; i++) {
|
|
95
|
+
const a = chroma * Math.cos(hRad);
|
|
96
|
+
const b = chroma * Math.sin(hRad);
|
|
97
|
+
const [r, g, bl] = oklabToLinearRgb(l, a, b);
|
|
98
|
+
const inGamut = [r, g, bl].every((v) => v >= -0.001 && v <= 1.001);
|
|
99
|
+
if (inGamut || chroma <= 0.0001) {
|
|
100
|
+
return rgbToHex(linearToSrgb8(r), linearToSrgb8(g), linearToSrgb8(bl));
|
|
101
|
+
}
|
|
102
|
+
chroma *= 0.92;
|
|
103
|
+
}
|
|
104
|
+
const [r, g, bl] = oklabToLinearRgb(l, 0, 0);
|
|
105
|
+
return rgbToHex(linearToSrgb8(r), linearToSrgb8(g), linearToSrgb8(bl));
|
|
106
|
+
}
|
|
107
|
+
/** WCAG göreli parlaklık (0-1) — `pickForeground`'ın kontrast oranı hesabının temeli. (`npm run check:contrast`, `scripts/check-contrast.mjs`, kitten BAĞIMSIZ kendi kopyasını taşır — aynı formül, farklı dosya.) */
|
|
108
|
+
function relativeLuminance(hex) {
|
|
109
|
+
const [r8, g8, b8] = hexToRgb(hex);
|
|
110
|
+
const lin = (c8) => {
|
|
111
|
+
const v = c8 / 255;
|
|
112
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
113
|
+
};
|
|
114
|
+
return 0.2126 * lin(r8) + 0.7152 * lin(g8) + 0.0722 * lin(b8);
|
|
115
|
+
}
|
|
116
|
+
/** İki hex arası WCAG kontrast oranı (1-21). */
|
|
117
|
+
function contrastRatio(hexA, hexB) {
|
|
118
|
+
const lA = relativeLuminance(hexA);
|
|
119
|
+
const lB = relativeLuminance(hexB);
|
|
120
|
+
const lighter = Math.max(lA, lB);
|
|
121
|
+
const darker = Math.min(lA, lB);
|
|
122
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
123
|
+
}
|
|
124
|
+
/** `background` üstünde `light`/`dark` metninden HANGİSİ daha kontrastlıysa onu döner — `fgOnAccent` bunu kullanır. */
|
|
125
|
+
function pickForeground(background, light, dark) {
|
|
126
|
+
return contrastRatio(background, light) >= contrastRatio(background, dark) ? light : dark;
|
|
127
|
+
}
|
|
128
|
+
/** `a`/`b` hex'lerini `t` (0-1) oranında karıştırır — `neutralTint`'in ince renk katması için (OKLCH DEĞİL, düz sRGB lerp: küçük `t` değerlerinde (≤0.08) fark edilmez, 13 basamaklı nötr rampayı yeniden OKLCH'de üretmekten çok daha basit ve güvenli). */
|
|
129
|
+
function mixHex(a, b, t) {
|
|
130
|
+
const [ar, ag, ab] = hexToRgb(a);
|
|
131
|
+
const [br, bg, bb] = hexToRgb(b);
|
|
132
|
+
const mix = (x, y) => x + (y - x) * t;
|
|
133
|
+
return rgbToHex(mix(ar, br), mix(ag, bg), mix(ab, bb));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Basamak bağımsız SABİT bir eğri — herhangi bir girdi hue'sunda "aynı ağırlık
|
|
138
|
+
* hissini" garanti eder (bkz. plan Faz 2: tüketici hangi accent'i seçerse
|
|
139
|
+
* seçsin ramp'in 600 basamağı HER ZAMAN "metin/kenarlık" ağırlığında olmalı,
|
|
140
|
+
* 950 basamağı HER ZAMAN "neredeyse siyah" olmalı). `color.css`'teki
|
|
141
|
+
* varsayılan "Deep Emerald" rampasıyla (primary-600 = #004d40, L≈0.38)
|
|
142
|
+
* TAM ÖRTÜŞMEZ — bu eğride 600 = L 0.50, kasıtlı: elle taşınan varsayılan
|
|
143
|
+
* rampa marka için özel olarak koyulaştırıldı. Ayrıca DOLU BLOK (`accent-fill`)
|
|
144
|
+
* artık `accent`/`accent-strong` (600/700 tabanlı) metin/kenarlık rolünden
|
|
145
|
+
* TAMAMEN AYRI bir basamak çifti kullanıyor — kitin varsayılan rampasında
|
|
146
|
+
* LIGHT 900 / DARK 400 (spec'in "Forest Emerald" CTA'sı + canlı zümrüt CTA'sı,
|
|
147
|
+
* kullanıcı kararı); tüketici aksanlarında ise (bkz. `theming/apply.ts`
|
|
148
|
+
* `accentRoleLines()`) bu eğrinin kendi 600/400 basamakları kullanılır —
|
|
149
|
+
* varsayılandan farklı bir "ağırlık" üretir, bu FARK belgelidir, hata değildir.
|
|
150
|
+
*/
|
|
151
|
+
const LIGHTNESS_BY_STEP = {
|
|
152
|
+
'50': 0.97,
|
|
153
|
+
'100': 0.94,
|
|
154
|
+
'200': 0.88,
|
|
155
|
+
'300': 0.79,
|
|
156
|
+
'400': 0.7,
|
|
157
|
+
'500': 0.6,
|
|
158
|
+
'600': 0.5,
|
|
159
|
+
'700': 0.42,
|
|
160
|
+
'800': 0.34,
|
|
161
|
+
'900': 0.27,
|
|
162
|
+
'950': 0.2,
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Girdinin KENDİ kroması, uç basamaklarda (çok açık/çok koyu) doğal olarak
|
|
166
|
+
* azalır — aksi halde `950` basamağı "doygun ama neredeyse siyah" (gamut
|
|
167
|
+
* dışı, kirpma sonrası tonu kayan) bir renge dönüşür. `600` = girdinin TAM
|
|
168
|
+
* kroması (referans basamak).
|
|
169
|
+
*/
|
|
170
|
+
const CHROMA_MULTIPLIER_BY_STEP = {
|
|
171
|
+
'50': 0.05,
|
|
172
|
+
'100': 0.12,
|
|
173
|
+
'200': 0.25,
|
|
174
|
+
'300': 0.45,
|
|
175
|
+
'400': 0.7,
|
|
176
|
+
'500': 0.9,
|
|
177
|
+
'600': 1,
|
|
178
|
+
'700': 0.95,
|
|
179
|
+
'800': 0.85,
|
|
180
|
+
'900': 0.7,
|
|
181
|
+
'950': 0.55,
|
|
182
|
+
};
|
|
183
|
+
const RAMP_STEPS = [
|
|
184
|
+
'50',
|
|
185
|
+
'100',
|
|
186
|
+
'200',
|
|
187
|
+
'300',
|
|
188
|
+
'400',
|
|
189
|
+
'500',
|
|
190
|
+
'600',
|
|
191
|
+
'700',
|
|
192
|
+
'800',
|
|
193
|
+
'900',
|
|
194
|
+
'950',
|
|
195
|
+
];
|
|
196
|
+
/**
|
|
197
|
+
* `hex` bir accent'in "600 basamağı" gibi ele alınır (kitin kendi kuralı,
|
|
198
|
+
* bkz. `color.css` "Her rampada basamak 600 = LIGHT modda dolu/etkileşimli
|
|
199
|
+
* kullanım" notu) — girdinin hue'su + kroma karakteri korunur, tüm 11
|
|
200
|
+
* basamak `LIGHTNESS_BY_STEP`/`CHROMA_MULTIPLIER_BY_STEP` sabit eğrisiyle
|
|
201
|
+
* YENİDEN üretilir (girdinin TAM OLARAK 600'e denk gelen bir L'de olması
|
|
202
|
+
* ŞART DEĞİL — ör. çok açık bir hex verilse bile 950 basamağı yine
|
|
203
|
+
* "neredeyse siyah" çıkar, ramp her zaman tutarlı).
|
|
204
|
+
*
|
|
205
|
+
* @throws girdi `#rgb`/`#rrggbb` değilse.
|
|
206
|
+
*/
|
|
207
|
+
function createAccentRamp(hex) {
|
|
208
|
+
if (!isValidHex(hex)) {
|
|
209
|
+
throw new Error(`createAccentRamp: geçersiz hex "${hex}" — #rgb veya #rrggbb bekleniyor.`);
|
|
210
|
+
}
|
|
211
|
+
const { c, h } = hexToOklch(hex);
|
|
212
|
+
const ramp = {};
|
|
213
|
+
for (const step of RAMP_STEPS) {
|
|
214
|
+
ramp[step] = oklchToHex(LIGHTNESS_BY_STEP[step], c * CHROMA_MULTIPLIER_BY_STEP[step], h);
|
|
215
|
+
}
|
|
216
|
+
return ramp;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Bir rampa basamağının üstüne binecek metin rengi — `#ffffff` veya kitin
|
|
220
|
+
* `--mf-surface-inverse`'i (`#04110d`, `color.css`) arasından HANGİSİ WCAG
|
|
221
|
+
* kontrastı daha yüksekse. `theming/apply.ts`'teki `accentRoleLines()` bunu
|
|
222
|
+
* `ramp['600']` (LIGHT yarı) VE `ramp['400']` (DARK yarı) için AYRI AYRI
|
|
223
|
+
* çağırır — accent-fill artık İKİ basamak kullanıyor (kitin varsayılanında
|
|
224
|
+
* 900/400, tüketici aksanlarında 600/400), genelde çok farklı parlaklıkta,
|
|
225
|
+
* her biri kendi doğru metin rengini WCAG hesabıyla (sabit varsayım DEĞİL)
|
|
226
|
+
* bulur.
|
|
227
|
+
*/
|
|
228
|
+
function fgOnAccent(accentHex) {
|
|
229
|
+
return pickForeground(accentHex, '#ffffff', '#04110d');
|
|
230
|
+
}
|
|
231
|
+
/** `ramp`'in `600` basamağının kontrast oranını (beyaz metne karşı) döner — `ramp.spec.ts`/tüketici sağlık kontrolü için. */
|
|
232
|
+
function accentContrastWithWhite(ramp) {
|
|
233
|
+
return contrastRatio(ramp['600'], '#ffffff');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Nötr rampa 13 basamaklı (`850`/`975` DAHİL) — diğer altı rampanın (primary/
|
|
238
|
+
* success/warning/danger/info/help) 11 basamaklık `RampStep`'inden AYRI bir
|
|
239
|
+
* tip (bkz. `color.css` "RENK — HAM RAMPALAR" notu, `NEUTRAL_STEPS` — `apply.ts`).
|
|
240
|
+
*/
|
|
241
|
+
const MF_NEUTRAL_RAMP_STEPS = [
|
|
242
|
+
'50',
|
|
243
|
+
'100',
|
|
244
|
+
'200',
|
|
245
|
+
'300',
|
|
246
|
+
'400',
|
|
247
|
+
'500',
|
|
248
|
+
'600',
|
|
249
|
+
'700',
|
|
250
|
+
'800',
|
|
251
|
+
'850',
|
|
252
|
+
'900',
|
|
253
|
+
'950',
|
|
254
|
+
'975',
|
|
255
|
+
];
|
|
256
|
+
/** `ramps.<tone>` altında elle geçilebilen altı 11-basamaklı rampa — `neutral` (13 basamak) AYRI ele alınır. */
|
|
257
|
+
const MF_RAMP_TONES = ['primary', 'success', 'warning', 'danger', 'info', 'help'];
|
|
258
|
+
/**
|
|
259
|
+
* `color.css`'in rol katmanındaki, bir RAMPA basamağına `var()` İLE
|
|
260
|
+
* BAĞLI OLMAYAN (yani `ramps.*` override'ıyla OTOMATİK takip ETMEYEN) TEK
|
|
261
|
+
* TEK yazılmış literal token'lar — `accent`/`ramps.*` DIŞINDA kalan, marka
|
|
262
|
+
* beyaz etiketlemesi için hâlâ ayarlanabilir olması gereken son katman.
|
|
263
|
+
* (`--mf-accent`, `--mf-success-fill`, `--mf-success-tint` vb. BURADA YOK —
|
|
264
|
+
* hepsi zaten `ramps.primary`/`ramps.success` override'ıyla otomatik
|
|
265
|
+
* güncellenir, bkz. `color.css`'teki `var(--color-X-NNN)` referansları.)
|
|
266
|
+
*/
|
|
267
|
+
const MF_ROLE_TOKENS = [
|
|
268
|
+
'surface',
|
|
269
|
+
'chrome',
|
|
270
|
+
'overlay',
|
|
271
|
+
'surface-inverse',
|
|
272
|
+
'glass',
|
|
273
|
+
'fg',
|
|
274
|
+
'fg-muted',
|
|
275
|
+
'fg-on-accent',
|
|
276
|
+
'fg-inverse',
|
|
277
|
+
'hairline',
|
|
278
|
+
'neutral-fill',
|
|
279
|
+
'neutral-fill-hover',
|
|
280
|
+
'neutral-fill-fg',
|
|
281
|
+
'success-fill-fg',
|
|
282
|
+
'warning-fill-fg',
|
|
283
|
+
'danger-fill-fg',
|
|
284
|
+
'info-fill-fg',
|
|
285
|
+
'help-fill-fg',
|
|
286
|
+
'chart-1',
|
|
287
|
+
'chart-2',
|
|
288
|
+
'chart-3',
|
|
289
|
+
'chart-4',
|
|
290
|
+
'chart-5',
|
|
291
|
+
'chart-6',
|
|
292
|
+
'chart-7',
|
|
293
|
+
'chart-8',
|
|
294
|
+
'chart-other',
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
const RADIUS_PX = {
|
|
298
|
+
sharp: { xs: 2, sm: 3, md: 4, lg: 6, xl: 8 },
|
|
299
|
+
default: { xs: 4, sm: 6, md: 8, lg: 12, xl: 16 },
|
|
300
|
+
soft: { xs: 6, sm: 10, md: 14, lg: 20, xl: 28 },
|
|
301
|
+
};
|
|
302
|
+
/** `radius.css`'teki `--control-h-*` ile AYNI değerler — `[data-density]` seçicileri de bunları kullanır. */
|
|
303
|
+
const CONTROL_H_PX = {
|
|
304
|
+
compact: { sm: 28, md: 32, lg: 36 },
|
|
305
|
+
default: { sm: 32, md: 36, lg: 40 },
|
|
306
|
+
comfortable: { sm: 36, md: 40, lg: 44 },
|
|
307
|
+
};
|
|
308
|
+
const CONTROL_H_KEYS = ['sm', 'md', 'lg'];
|
|
309
|
+
function densityLines(density) {
|
|
310
|
+
return CONTROL_H_KEYS.map((key) => [
|
|
311
|
+
`--control-h-${key}`,
|
|
312
|
+
`${CONTROL_H_PX[density][key] / 16}rem`,
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
/** `color.css`'teki 13 basamaklı nötr merdiven — tint karışımı bunların HEPSİNİ, moddan bağımsız (raw ramp) günceller. */
|
|
316
|
+
const NEUTRAL_STEPS = MF_NEUTRAL_RAMP_STEPS;
|
|
317
|
+
/** `ramps.<tone>` genel-amaçlı döngüsü için — `primary` kendi özel `accentRoleLines()` türetmesi yüzünden AYRI ele alınır (bkz. altta). */
|
|
318
|
+
const OTHER_RAMP_TONES = MF_RAMP_TONES.filter((tone) => tone !== 'primary');
|
|
319
|
+
/** Sabit, hue'ya dayalı iki tint ucu — accent'ten BAĞIMSIZ (bkz. plan Faz 2 "neutralTint"). `mixHex` küçük `t` ile çağrıldığı için nötrün kendi algısal parlaklığı korunur. */
|
|
320
|
+
const TINT_HEX = {
|
|
321
|
+
cool: '#3b82f6',
|
|
322
|
+
warm: '#f59e0b',
|
|
323
|
+
};
|
|
324
|
+
const TINT_AMOUNT = 0.06;
|
|
325
|
+
/**
|
|
326
|
+
* `--mf-fg-on-accent`/`--mf-accent-fill`/`--mf-accent-fill-hover` rol
|
|
327
|
+
* token'ları rampa basamağı gibi `--color-primary-*` ADI ALTINDA değil,
|
|
328
|
+
* kendi `--mf-*` isimleriyle yazılır (bkz. color.css rol katmanı) — bu
|
|
329
|
+
* yüzden `RAMP_STEPS` döngüsüne dahil DEĞİLLER, `accent: null` temizliğinin
|
|
330
|
+
* de bunları AYRICA silmesi gerekiyor.
|
|
331
|
+
*/
|
|
332
|
+
const ACCENT_ROLE_PROPS = [
|
|
333
|
+
'--mf-fg-on-accent',
|
|
334
|
+
'--mf-accent-fill',
|
|
335
|
+
'--mf-accent-fill-hover',
|
|
336
|
+
];
|
|
337
|
+
/**
|
|
338
|
+
* Bir accent ramp'inden üç rol token satırını üretir — `applyMfTheme` (runtime
|
|
339
|
+
* `setProperty`) ve `exportMfThemeCss` (statik string) İKİSİ de bunu kullanır,
|
|
340
|
+
* tek kaynak. Kitin KENDİ varsayılan rampasıyla AYNI desen (color.css): dolu
|
|
341
|
+
* blok LIGHT'ta 600, DARK'ta 400 (hover 700/500) — `light-dark()`'ın İKİ
|
|
342
|
+
* yarısı da AYRI hesaplanır, tek bir sabit değer DEĞİL. `fgOnAccent` her
|
|
343
|
+
* yarı için AYRI çağrılır çünkü 600 ile 400 basamağı genelde çok farklı
|
|
344
|
+
* parlaklıkta (600 koyu → genelde beyaz metin ister, 400 açık → genelde
|
|
345
|
+
* mürekkep ister) — tüketicinin verdiği HERHANGİ bir aksan renginde bu iki
|
|
346
|
+
* yarı FARKLI metin rengine ihtiyaç duyabilir (bkz. kitin varsayılan primary
|
|
347
|
+
* rampası: light `#062c22` + beyaz, dark `#10b981` + mürekkep — aynı asimetri).
|
|
348
|
+
* `ramp` EKSİK basamak taşıyorsa (ör. `ramps.primary` kısmi verildiyse,
|
|
349
|
+
* `resolvedPrimaryRamp`'in okunmuş/birleştirilmiş sonucu DEĞİLSE) `null`
|
|
350
|
+
* döner — çağıran bu durumda satırları YAZMAZ (bkz. `exportMfThemeCss`).
|
|
351
|
+
*/
|
|
352
|
+
function accentRoleLines(ramp) {
|
|
353
|
+
const c400 = ramp['400'];
|
|
354
|
+
const c500 = ramp['500'];
|
|
355
|
+
const c600 = ramp['600'];
|
|
356
|
+
const c700 = ramp['700'];
|
|
357
|
+
if (!c400 || !c500 || !c600 || !c700)
|
|
358
|
+
return null;
|
|
359
|
+
const fgLight = fgOnAccent(c600);
|
|
360
|
+
const fgDark = fgOnAccent(c400);
|
|
361
|
+
return [
|
|
362
|
+
['--mf-fg-on-accent', `light-dark(${fgLight}, ${fgDark})`],
|
|
363
|
+
['--mf-accent-fill', `light-dark(${c600}, ${c400})`],
|
|
364
|
+
['--mf-accent-fill-hover', `light-dark(${c700}, ${c500})`],
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
function isBrowser() {
|
|
368
|
+
return typeof document !== 'undefined';
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* `config.ramps.primary` (kısmi olabilir) ile `el`'in O ANKİ hesaplanmış
|
|
372
|
+
* `--color-primary-*` değerlerini BİRLEŞTİRİR — `accentRoleLines()`'ın TAM
|
|
373
|
+
* bir ramp'e (400/500/600/700) ihtiyacı var, tüketici SADECE birkaç basamağı
|
|
374
|
+
* override etse bile (ör. sadece `600`) doğru `--mf-accent-fill` vb.
|
|
375
|
+
* türetilsin diye (bkz. `neutralTint`'in AYNI "önce oku, sonra karıştır"
|
|
376
|
+
* ilkesi, altta).
|
|
377
|
+
*/
|
|
378
|
+
function resolvedPrimaryRamp(el, override) {
|
|
379
|
+
const computed = getComputedStyle(el);
|
|
380
|
+
const ramp = {};
|
|
381
|
+
for (const step of RAMP_STEPS) {
|
|
382
|
+
ramp[step] = override[step] ?? computed.getPropertyValue(`--color-primary-${step}`).trim();
|
|
383
|
+
}
|
|
384
|
+
return ramp;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* `config.roles.light[key]`/`config.roles.dark[key]`'i TEK bir `light-dark()`
|
|
388
|
+
* CSS değerine çevirir — sadece BİRİ verilirse İKİ modda da AYNI değer
|
|
389
|
+
* kullanılır (bkz. `theme-json.ts`'teki `MfThemeJson.roles` doc'u — hangi
|
|
390
|
+
* modda olunduğunu PROBE etmek yerine BİLEREK basit/öngörülebilir).
|
|
391
|
+
*/
|
|
392
|
+
function roleLightDarkValue(light, dark) {
|
|
393
|
+
const resolvedLight = light ?? dark;
|
|
394
|
+
const resolvedDark = dark ?? light;
|
|
395
|
+
if (resolvedLight === undefined || resolvedDark === undefined)
|
|
396
|
+
return null;
|
|
397
|
+
return `light-dark(${resolvedLight}, ${resolvedDark})`;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* `MfThemeJson`'ı CSS custom property'leri olarak `target`'a (varsayılan
|
|
401
|
+
* `document.documentElement`) YAZAR — sayfa YENİDEN YÜKLENMEDEN, hiçbir
|
|
402
|
+
* bileşen dosyasına dokunmadan tüm kit anında yeni temaya geçer. Bunun
|
|
403
|
+
* mümkün olmasının nedeni: Tailwind v4'ün ürettiği utility'ler (`bg-primary-600`,
|
|
404
|
+
* `rounded-md`, ...) derleme zamanında SABİT bir değere değil, doğrudan
|
|
405
|
+
* `var(--color-primary-600)`/`var(--radius-md)` REFERANSINA derleniyor (bkz.
|
|
406
|
+
* `docs/tr/mimari.md` "Token mimarisi") — CSS cascade'de bu satırlar
|
|
407
|
+
* `@theme`'in KENDİ tanımından SONRA (inline stil her zaman kazanır)
|
|
408
|
+
* çalıştığı için token'lar üzerinden TÜM bileşenler güncellenir. SADECE
|
|
409
|
+
* `config`'te VERİLEN alanlar yazılır ("delta-only") — bu fonksiyon kitin
|
|
410
|
+
* TÜM varsayılan token setini bilmek/üretmek ZORUNDA DEĞİL, bu yüzden
|
|
411
|
+
* kitin CSS kaynağıyla asla senkron dışı kalamaz.
|
|
412
|
+
*
|
|
413
|
+
* - `accent` verilirse: `createAccentRamp()` ile 11 basamaklı `--color-primary-*`
|
|
414
|
+
* YENİDEN üretilir + `accentRoleLines()` (`--mf-fg-on-accent`/`-accent-fill`/
|
|
415
|
+
* `-accent-fill-hover`, `light-dark()` literal değerleri — bkz. altta).
|
|
416
|
+
* `:root`/`.dark` bunları zaten `var()` ile referans aldığı için (bkz.
|
|
417
|
+
* `color.css` rol katmanı) `--mf-accent`/`--mf-accent-strong`/`--mf-focus-ring`/
|
|
418
|
+
* `--mf-primary-tint(-fg)` AYRICA yazılmaz — otomatik takip eder.
|
|
419
|
+
* - `accent: null` verilirse: yukarıdaki override'ların HEPSİ temizlenir, kit
|
|
420
|
+
* `color.css`'teki varsayılan "Deep Emerald" rampasına döner.
|
|
421
|
+
* - `ramps.primary` verilirse (`accent`'i EZER — aynı çağrıda ikisi de
|
|
422
|
+
* verilirse `ramps.primary` kazanır): basamak başına TAM kontrol, kısmi
|
|
423
|
+
* olabilir (`resolvedPrimaryRamp` ile O ANKİ değerlerle birleştirilir).
|
|
424
|
+
* - `ramps.success`/`warning`/`danger`/`info`/`help`/`neutral` verilirse:
|
|
425
|
+
* verilen basamaklar DOĞRUDAN yazılır — bu tonların TÜM türetilmiş rol
|
|
426
|
+
* token'ları (`--mf-X`, `--mf-X-fill`, `--mf-X-tint` vb.) `color.css`'te
|
|
427
|
+
* zaten `var(--color-X-NNN)` ile bu rampaya bağlı, OTOMATİK güncellenir.
|
|
428
|
+
* - `roles.light`/`roles.dark` verilirse: rampaya BAĞLI OLMAYAN literal
|
|
429
|
+
* token'lar (bkz. `MfRoleToken`) `light-dark()` ile yazılır.
|
|
430
|
+
* - `fonts`/`radius` verilirse: moddan bağımsız düz değerler yazılır —
|
|
431
|
+
* `radius` `radiusScale`'İN ÜSTÜNE (aynı çağrıda ikisi de verilirse SONRA
|
|
432
|
+
* uygulanır) basamak-bazlı ince ayar sağlar.
|
|
433
|
+
* - `radiusScale` verilirse: `--radius-{xs,sm,md,lg,xl}` nested ölçeği
|
|
434
|
+
* KORUYARAK (bkz. `radius.css` doktrini) birlikte küçülür/büyür.
|
|
435
|
+
* - `density` verilirse: `--control-h-{sm,md,lg}` merdiveni yazılır — tüm
|
|
436
|
+
* kontroller (Button/Input/Select/...) aynı hizada birlikte büyür/küçülür.
|
|
437
|
+
* - `neutralTint` verilirse: 13 basamaklı `--color-neutral-*` rampası ÇOK
|
|
438
|
+
* düşük oranda (bkz. `color-math.ts` `mixHex`) soğuk/sıcak bir tona kayar
|
|
439
|
+
* — `ramps.neutral` AYNI ÇAĞRIDA verildiyse ONUN ÜSTÜNE uygulanır (bu
|
|
440
|
+
* fonksiyondaki sıra: ramps → neutralTint).
|
|
441
|
+
*
|
|
442
|
+
* SSR güvenli — `document` yoksa no-op.
|
|
443
|
+
*/
|
|
444
|
+
function applyMfTheme(config, target) {
|
|
445
|
+
if (!isBrowser())
|
|
446
|
+
return;
|
|
447
|
+
const el = target ?? document.documentElement;
|
|
448
|
+
if (config.accent === null && !config.ramps?.primary) {
|
|
449
|
+
for (const step of RAMP_STEPS) {
|
|
450
|
+
el.style.removeProperty(`--color-primary-${step}`);
|
|
451
|
+
}
|
|
452
|
+
for (const prop of ACCENT_ROLE_PROPS) {
|
|
453
|
+
el.style.removeProperty(prop);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
else if (config.ramps?.primary) {
|
|
457
|
+
const ramp = resolvedPrimaryRamp(el, config.ramps.primary);
|
|
458
|
+
for (const step of RAMP_STEPS) {
|
|
459
|
+
el.style.setProperty(`--color-primary-${step}`, ramp[step]);
|
|
460
|
+
}
|
|
461
|
+
const lines = accentRoleLines(ramp);
|
|
462
|
+
if (lines) {
|
|
463
|
+
for (const [prop, value] of lines)
|
|
464
|
+
el.style.setProperty(prop, value);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else if (config.accent) {
|
|
468
|
+
const ramp = createAccentRamp(config.accent);
|
|
469
|
+
for (const step of RAMP_STEPS) {
|
|
470
|
+
el.style.setProperty(`--color-primary-${step}`, ramp[step]);
|
|
471
|
+
}
|
|
472
|
+
const lines = accentRoleLines(ramp);
|
|
473
|
+
if (lines) {
|
|
474
|
+
for (const [prop, value] of lines)
|
|
475
|
+
el.style.setProperty(prop, value);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
for (const tone of OTHER_RAMP_TONES) {
|
|
479
|
+
const override = config.ramps?.[tone];
|
|
480
|
+
if (!override)
|
|
481
|
+
continue;
|
|
482
|
+
for (const step of RAMP_STEPS) {
|
|
483
|
+
const hex = override[step];
|
|
484
|
+
if (hex)
|
|
485
|
+
el.style.setProperty(`--color-${tone}-${step}`, hex);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (config.ramps?.neutral) {
|
|
489
|
+
for (const step of NEUTRAL_STEPS) {
|
|
490
|
+
const hex = config.ramps.neutral[step];
|
|
491
|
+
if (hex)
|
|
492
|
+
el.style.setProperty(`--color-neutral-${step}`, hex);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (config.roles) {
|
|
496
|
+
for (const key of MF_ROLE_TOKENS) {
|
|
497
|
+
const value = roleLightDarkValue(config.roles.light?.[key], config.roles.dark?.[key]);
|
|
498
|
+
if (value)
|
|
499
|
+
el.style.setProperty(`--mf-${key}`, value);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (config.fonts?.sans)
|
|
503
|
+
el.style.setProperty('--font-sans', config.fonts.sans);
|
|
504
|
+
if (config.fonts?.display)
|
|
505
|
+
el.style.setProperty('--font-display', config.fonts.display);
|
|
506
|
+
if (config.fonts?.mono)
|
|
507
|
+
el.style.setProperty('--font-mono', config.fonts.mono);
|
|
508
|
+
if (config.radiusScale) {
|
|
509
|
+
const px = RADIUS_PX[config.radiusScale];
|
|
510
|
+
for (const [key, value] of Object.entries(px)) {
|
|
511
|
+
el.style.setProperty(`--radius-${key}`, `${value}px`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (config.radius) {
|
|
515
|
+
for (const [key, value] of Object.entries(config.radius)) {
|
|
516
|
+
if (value)
|
|
517
|
+
el.style.setProperty(`--radius-${key}`, value);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (config.density) {
|
|
521
|
+
for (const [prop, value] of densityLines(config.density))
|
|
522
|
+
el.style.setProperty(prop, value);
|
|
523
|
+
}
|
|
524
|
+
if (config.neutralTint !== undefined) {
|
|
525
|
+
// ÖNCE inline override'ı TEMİZLE (`removeProperty`), SONRA (tint !=
|
|
526
|
+
// 'neutral' ise) `getComputedStyle`'ı yeniden oku — aksi halde art arda
|
|
527
|
+
// iki `applyMfTheme({ neutralTint: 'cool' })` çağrısı (ör. tema
|
|
528
|
+
// özelleştiricide kullanıcı aynı seçimi tekrar tetiklerse) İKİNCİ
|
|
529
|
+
// çağrıda ZATEN tint'lenmiş değerin üstüne BİR TINT DAHA karıştırırdı
|
|
530
|
+
// (`getComputedStyle` kendi az önce yazdığı inline değeri geri okur) —
|
|
531
|
+
// gerçek bulgu, kümülatif/katlanan bir renk kayması üretiyordu.
|
|
532
|
+
for (const step of NEUTRAL_STEPS) {
|
|
533
|
+
el.style.removeProperty(`--color-neutral-${step}`);
|
|
534
|
+
}
|
|
535
|
+
if (config.neutralTint !== 'neutral') {
|
|
536
|
+
const tint = TINT_HEX[config.neutralTint];
|
|
537
|
+
const computed = getComputedStyle(el);
|
|
538
|
+
for (const step of NEUTRAL_STEPS) {
|
|
539
|
+
const current = computed.getPropertyValue(`--color-neutral-${step}`).trim();
|
|
540
|
+
if (!current)
|
|
541
|
+
continue;
|
|
542
|
+
el.style.setProperty(`--color-neutral-${step}`, mixHex(current, tint, TINT_AMOUNT));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* `applyMfTheme()`'in YAZABİLECEĞİ HER inline override'ı temizler — bir
|
|
549
|
+
* tema özelleştiricideki "Varsayılana sıfırla" düğmesi için `accent: null`'ın
|
|
550
|
+
* genişletilmiş hali (o SADECE accent'i temizlerdi). Kit `color.css`'teki
|
|
551
|
+
* varsayılanlara TAMAMEN geri döner.
|
|
552
|
+
*/
|
|
553
|
+
function resetMfTheme(target) {
|
|
554
|
+
if (!isBrowser())
|
|
555
|
+
return;
|
|
556
|
+
const el = target ?? document.documentElement;
|
|
557
|
+
for (const step of RAMP_STEPS)
|
|
558
|
+
el.style.removeProperty(`--color-primary-${step}`);
|
|
559
|
+
for (const prop of ACCENT_ROLE_PROPS)
|
|
560
|
+
el.style.removeProperty(prop);
|
|
561
|
+
for (const tone of OTHER_RAMP_TONES) {
|
|
562
|
+
for (const step of RAMP_STEPS)
|
|
563
|
+
el.style.removeProperty(`--color-${tone}-${step}`);
|
|
564
|
+
}
|
|
565
|
+
for (const step of NEUTRAL_STEPS)
|
|
566
|
+
el.style.removeProperty(`--color-neutral-${step}`);
|
|
567
|
+
for (const key of MF_ROLE_TOKENS)
|
|
568
|
+
el.style.removeProperty(`--mf-${key}`);
|
|
569
|
+
el.style.removeProperty('--font-sans');
|
|
570
|
+
el.style.removeProperty('--font-display');
|
|
571
|
+
el.style.removeProperty('--font-mono');
|
|
572
|
+
for (const key of ['xs', 'sm', 'md', 'lg', 'xl'])
|
|
573
|
+
el.style.removeProperty(`--radius-${key}`);
|
|
574
|
+
for (const key of CONTROL_H_KEYS)
|
|
575
|
+
el.style.removeProperty(`--control-h-${key}`);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* `applyMfTheme` ile AYNI hesaplamayı yapar ama runtime'da uygulamak yerine
|
|
579
|
+
* yapıştırılabilir bir `{ selector } { ... }` bloğu (varsayılan `:root`)
|
|
580
|
+
* döner — tüketici build-time'da kendi `styles.css`'ine ekleyebilir (docs
|
|
581
|
+
* sitesindeki tema özelleştiricinin "CSS'i kopyala" düğmesi bunu kullanır).
|
|
582
|
+
* `roles.*` İÇİN de (runtime'daki AYNI `light-dark()` deseni) tek blokla
|
|
583
|
+
* hem açık hem koyu mod kapsanır, AYRI bir `.dark` bloğu GEREKMEZ. `ramps.
|
|
584
|
+
* primary` KISMİ verilip 400/500/600/700'ün TAMAMI yoksa `--mf-accent-fill`
|
|
585
|
+
* vb. satırlar (canlı DOM'dan O ANKİ değeri okuyamadığı için) ATLANIR — bu
|
|
586
|
+
* durumda `applyMfTheme`'i (runtime, `resolvedPrimaryRamp` ile tamamlar)
|
|
587
|
+
* tercih edin. `neutralTint` BURADA desteklenmez — mevcut nötr değerleri
|
|
588
|
+
* okumak için canlı bir DOM/`getComputedStyle` gerekir, statik bir string
|
|
589
|
+
* üretici bunu bilemez (tüketici tint'i kendi `applyMfTheme()` çağrısıyla
|
|
590
|
+
* runtime'da uygulamalı).
|
|
591
|
+
*/
|
|
592
|
+
function exportMfThemeCss(config, options) {
|
|
593
|
+
const selector = options?.selector ?? ':root';
|
|
594
|
+
const lines = [`${selector} {`];
|
|
595
|
+
if (config.ramps?.primary) {
|
|
596
|
+
const ramp = config.ramps.primary;
|
|
597
|
+
for (const step of RAMP_STEPS) {
|
|
598
|
+
const hex = ramp[step];
|
|
599
|
+
if (hex)
|
|
600
|
+
lines.push(` --color-primary-${step}: ${hex};`);
|
|
601
|
+
}
|
|
602
|
+
const roleLines = accentRoleLines(ramp);
|
|
603
|
+
if (roleLines) {
|
|
604
|
+
for (const [prop, value] of roleLines)
|
|
605
|
+
lines.push(` ${prop}: ${value};`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else if (config.accent) {
|
|
609
|
+
const ramp = createAccentRamp(config.accent);
|
|
610
|
+
for (const step of RAMP_STEPS) {
|
|
611
|
+
lines.push(` --color-primary-${step}: ${ramp[step]};`);
|
|
612
|
+
}
|
|
613
|
+
const roleLines = accentRoleLines(ramp);
|
|
614
|
+
if (roleLines) {
|
|
615
|
+
for (const [prop, value] of roleLines)
|
|
616
|
+
lines.push(` ${prop}: ${value};`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
for (const tone of OTHER_RAMP_TONES) {
|
|
620
|
+
const override = config.ramps?.[tone];
|
|
621
|
+
if (!override)
|
|
622
|
+
continue;
|
|
623
|
+
for (const step of RAMP_STEPS) {
|
|
624
|
+
const hex = override[step];
|
|
625
|
+
if (hex)
|
|
626
|
+
lines.push(` --color-${tone}-${step}: ${hex};`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (config.ramps?.neutral) {
|
|
630
|
+
for (const step of NEUTRAL_STEPS) {
|
|
631
|
+
const hex = config.ramps.neutral[step];
|
|
632
|
+
if (hex)
|
|
633
|
+
lines.push(` --color-neutral-${step}: ${hex};`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (config.roles) {
|
|
637
|
+
for (const key of MF_ROLE_TOKENS) {
|
|
638
|
+
const value = roleLightDarkValue(config.roles.light?.[key], config.roles.dark?.[key]);
|
|
639
|
+
if (value)
|
|
640
|
+
lines.push(` --mf-${key}: ${value};`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (config.fonts?.sans)
|
|
644
|
+
lines.push(` --font-sans: ${config.fonts.sans};`);
|
|
645
|
+
if (config.fonts?.display)
|
|
646
|
+
lines.push(` --font-display: ${config.fonts.display};`);
|
|
647
|
+
if (config.fonts?.mono)
|
|
648
|
+
lines.push(` --font-mono: ${config.fonts.mono};`);
|
|
649
|
+
if (config.radiusScale) {
|
|
650
|
+
const px = RADIUS_PX[config.radiusScale];
|
|
651
|
+
for (const [key, value] of Object.entries(px)) {
|
|
652
|
+
lines.push(` --radius-${key}: ${value}px;`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (config.radius) {
|
|
656
|
+
for (const [key, value] of Object.entries(config.radius)) {
|
|
657
|
+
if (value)
|
|
658
|
+
lines.push(` --radius-${key}: ${value};`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (config.density) {
|
|
662
|
+
for (const [prop, value] of densityLines(config.density))
|
|
663
|
+
lines.push(` ${prop}: ${value};`);
|
|
664
|
+
}
|
|
665
|
+
lines.push('}');
|
|
666
|
+
return lines.join('\n');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** `color.css`'in `--mf-surface-inverse`/`-fill-fg` literallerinde tekrarlanan "mürekkep" tonu — `X-fill-fg` HER ZAMAN bunun ya da beyazın arasından seçilir (bkz. rol katmanı). */
|
|
670
|
+
const INK = '#04110d';
|
|
671
|
+
const AA_NORMAL_TEXT = 4.5;
|
|
672
|
+
function checkHex(path, value, issues) {
|
|
673
|
+
if (value === undefined)
|
|
674
|
+
return;
|
|
675
|
+
if (!isValidHex(value)) {
|
|
676
|
+
issues.push({
|
|
677
|
+
path,
|
|
678
|
+
message: `"${value}" geçerli bir #rgb/#rrggbb hex değeri değil.`,
|
|
679
|
+
severity: 'error',
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* `MfThemeJson`'ı iki açıdan denetler:
|
|
685
|
+
*
|
|
686
|
+
* 1. Her hex alanının GEÇERLİ bir `#rgb`/`#rrggbb` olup olmadığı.
|
|
687
|
+
* 2. `ramps.<tone>['600']` (kitin "dolu blok" LIGHT çapası, bkz. `color.css`
|
|
688
|
+
* rol katmanı doktrini — `X-fill-fg` her zaman `#ffffff` ya da mürekkep
|
|
689
|
+
* `#04110d`'den seçilir) verilen bir ton için NE beyaz NE mürekkep
|
|
690
|
+
* üstünde WCAG AA (4.5:1) sağlayan okunaklı bir metin rengi bulunup
|
|
691
|
+
* bulunamadığı — ikisi de başarısızsa bu basamak "çok orta tonlu", dolu
|
|
692
|
+
* blok üstünde okunaklı metin GARANTİSİ yok demektir. `accent` (`ramps.
|
|
693
|
+
* primary`/`createAccentRamp` + `fgOnAccent`) BU KONTROLE İHTİYAÇ
|
|
694
|
+
* DUYMAZ — `pickForeground` HER ZAMAN iki adaydan daha iyisini seçtiği
|
|
695
|
+
* için kontrast garantisi İNŞA GEREĞİ var (bkz. `apply.ts`
|
|
696
|
+
* `accentRoleLines`).
|
|
697
|
+
*
|
|
698
|
+
* Şema/yapı hatalarını (bilinmeyen alan vb.) DENETLEMEZ — TypeScript
|
|
699
|
+
* derleme zamanında zaten yakalar; `MfThemeJson` harici bir kaynaktan (ör.
|
|
700
|
+
* `JSON.parse`, bir tema dosyası yükleme formu) geliyorsa bu fonksiyon
|
|
701
|
+
* SADECE değer-düzeyi sorunları (geçersiz hex, düşük kontrast) yakalar.
|
|
702
|
+
*/
|
|
703
|
+
function validateMfTheme(theme) {
|
|
704
|
+
const issues = [];
|
|
705
|
+
checkHex('accent', theme.accent ?? undefined, issues);
|
|
706
|
+
for (const tone of MF_RAMP_TONES) {
|
|
707
|
+
const ramp = theme.ramps?.[tone];
|
|
708
|
+
if (!ramp)
|
|
709
|
+
continue;
|
|
710
|
+
for (const step of RAMP_STEPS) {
|
|
711
|
+
checkHex(`ramps.${tone}.${step}`, ramp[step], issues);
|
|
712
|
+
}
|
|
713
|
+
if (tone === 'primary')
|
|
714
|
+
continue;
|
|
715
|
+
const fill = ramp['600'];
|
|
716
|
+
if (!fill || !isValidHex(fill))
|
|
717
|
+
continue;
|
|
718
|
+
const onWhite = contrastRatio(fill, '#ffffff');
|
|
719
|
+
const onInk = contrastRatio(fill, INK);
|
|
720
|
+
if (onWhite < AA_NORMAL_TEXT && onInk < AA_NORMAL_TEXT) {
|
|
721
|
+
issues.push({
|
|
722
|
+
path: `ramps.${tone}.600`,
|
|
723
|
+
message: `"${fill}" dolu blok üstünde ne beyaz (${onWhite.toFixed(2)}:1) ne mürekkep (${onInk.toFixed(2)}:1) WCAG AA (4.5:1) sağlıyor — daha koyu/açık bir basamak seçin.`,
|
|
724
|
+
severity: 'warning',
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (theme.ramps?.neutral) {
|
|
729
|
+
for (const [step, value] of Object.entries(theme.ramps.neutral)) {
|
|
730
|
+
checkHex(`ramps.neutral.${step}`, value, issues);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (theme.roles) {
|
|
734
|
+
for (const [mode, values] of Object.entries(theme.roles)) {
|
|
735
|
+
if (!values)
|
|
736
|
+
continue;
|
|
737
|
+
for (const [key, value] of Object.entries(values)) {
|
|
738
|
+
checkHex(`roles.${mode}.${key}`, value, issues);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return issues;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* `MfThemeJson`'ın JSON Schema (2020-12) tanımı — IDE otomatik tamamlama
|
|
747
|
+
* (`"$schema"` alanı) ve harici doğrulayıcılar (ajv vb.) için. Düz bir JS
|
|
748
|
+
* nesnesi olarak dışa açılıyor — kitin sıfır çalışma zamanı bağımlılığı
|
|
749
|
+
* ilkesi gereği (bkz. `projects/ui-kit/README.md` "Bağımlılık politikası")
|
|
750
|
+
* hiçbir şema doğrulama KÜTÜPHANESİ KULLANILMIYOR/GEREKTİRİLMİYOR; asıl
|
|
751
|
+
* DEĞER-düzeyi doğrulama (geçerli hex, WCAG kontrastı) `validateMfTheme()`
|
|
752
|
+
* (bkz. `validate.ts`) tarafında, bu şema SADECE YAPISAL/tip doğrulaması
|
|
753
|
+
* içindir — `@mesgflow/email-core`'un `EMAIL_DOCUMENT_JSON_SCHEMA`'sıyla
|
|
754
|
+
* AYNI desen.
|
|
755
|
+
*/
|
|
756
|
+
const HEX_PATTERN = '^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$';
|
|
757
|
+
const HEX = { type: 'string', pattern: HEX_PATTERN };
|
|
758
|
+
const RAMP_11 = {
|
|
759
|
+
type: 'object',
|
|
760
|
+
additionalProperties: false,
|
|
761
|
+
properties: {
|
|
762
|
+
'50': HEX,
|
|
763
|
+
'100': HEX,
|
|
764
|
+
'200': HEX,
|
|
765
|
+
'300': HEX,
|
|
766
|
+
'400': HEX,
|
|
767
|
+
'500': HEX,
|
|
768
|
+
'600': HEX,
|
|
769
|
+
'700': HEX,
|
|
770
|
+
'800': HEX,
|
|
771
|
+
'900': HEX,
|
|
772
|
+
'950': HEX,
|
|
773
|
+
},
|
|
774
|
+
};
|
|
775
|
+
const RAMP_13 = {
|
|
776
|
+
type: 'object',
|
|
777
|
+
additionalProperties: false,
|
|
778
|
+
properties: {
|
|
779
|
+
...RAMP_11.properties,
|
|
780
|
+
'850': HEX,
|
|
781
|
+
'975': HEX,
|
|
782
|
+
},
|
|
783
|
+
};
|
|
784
|
+
const ROLE_TOKEN_NAMES = [
|
|
785
|
+
'surface',
|
|
786
|
+
'chrome',
|
|
787
|
+
'overlay',
|
|
788
|
+
'surface-inverse',
|
|
789
|
+
'glass',
|
|
790
|
+
'fg',
|
|
791
|
+
'fg-muted',
|
|
792
|
+
'fg-on-accent',
|
|
793
|
+
'fg-inverse',
|
|
794
|
+
'hairline',
|
|
795
|
+
'neutral-fill',
|
|
796
|
+
'neutral-fill-hover',
|
|
797
|
+
'neutral-fill-fg',
|
|
798
|
+
'success-fill-fg',
|
|
799
|
+
'warning-fill-fg',
|
|
800
|
+
'danger-fill-fg',
|
|
801
|
+
'info-fill-fg',
|
|
802
|
+
'help-fill-fg',
|
|
803
|
+
'chart-1',
|
|
804
|
+
'chart-2',
|
|
805
|
+
'chart-3',
|
|
806
|
+
'chart-4',
|
|
807
|
+
'chart-5',
|
|
808
|
+
'chart-6',
|
|
809
|
+
'chart-7',
|
|
810
|
+
'chart-8',
|
|
811
|
+
'chart-other',
|
|
812
|
+
];
|
|
813
|
+
const ROLE_MAP = {
|
|
814
|
+
type: 'object',
|
|
815
|
+
additionalProperties: false,
|
|
816
|
+
properties: Object.fromEntries(ROLE_TOKEN_NAMES.map((key) => [key, { type: 'string' }])),
|
|
817
|
+
};
|
|
818
|
+
const MF_THEME_JSON_SCHEMA = {
|
|
819
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
820
|
+
$id: 'https://hurdenizyener.github.io/mesgflow-ui-kit/schema/theme.json',
|
|
821
|
+
title: 'MesgFlow ui theme',
|
|
822
|
+
description: 'Tam beyaz etiket tema tanımı — applyMfTheme()/exportMfThemeCss()e verilir. Her alan opsiyonel.',
|
|
823
|
+
type: 'object',
|
|
824
|
+
additionalProperties: false,
|
|
825
|
+
properties: {
|
|
826
|
+
$schema: { type: 'string' },
|
|
827
|
+
name: { type: 'string' },
|
|
828
|
+
accent: { oneOf: [HEX, { type: 'null' }] },
|
|
829
|
+
radiusScale: { enum: ['sharp', 'default', 'soft'] },
|
|
830
|
+
density: { enum: ['compact', 'default', 'comfortable'] },
|
|
831
|
+
neutralTint: { enum: ['cool', 'neutral', 'warm'] },
|
|
832
|
+
ramps: {
|
|
833
|
+
type: 'object',
|
|
834
|
+
additionalProperties: false,
|
|
835
|
+
properties: {
|
|
836
|
+
primary: RAMP_11,
|
|
837
|
+
success: RAMP_11,
|
|
838
|
+
warning: RAMP_11,
|
|
839
|
+
danger: RAMP_11,
|
|
840
|
+
info: RAMP_11,
|
|
841
|
+
help: RAMP_11,
|
|
842
|
+
neutral: RAMP_13,
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
roles: {
|
|
846
|
+
type: 'object',
|
|
847
|
+
additionalProperties: false,
|
|
848
|
+
properties: {
|
|
849
|
+
light: ROLE_MAP,
|
|
850
|
+
dark: ROLE_MAP,
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
fonts: {
|
|
854
|
+
type: 'object',
|
|
855
|
+
additionalProperties: false,
|
|
856
|
+
properties: {
|
|
857
|
+
sans: { type: 'string' },
|
|
858
|
+
display: { type: 'string' },
|
|
859
|
+
mono: { type: 'string' },
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
radius: {
|
|
863
|
+
type: 'object',
|
|
864
|
+
additionalProperties: false,
|
|
865
|
+
properties: {
|
|
866
|
+
xs: { type: 'string' },
|
|
867
|
+
sm: { type: 'string' },
|
|
868
|
+
md: { type: 'string' },
|
|
869
|
+
lg: { type: 'string' },
|
|
870
|
+
xl: { type: 'string' },
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Tema değişikliğini (`.dark` toggle'ı, `applyMfTheme()` çağrısı vb.) View
|
|
878
|
+
* Transitions API ile yumuşak bir çapraz geçişe sarar — sayfa her token'ı
|
|
879
|
+
* tek tek `transition` etmek yerine eski/yeni ekran görüntüsü arasında TEK
|
|
880
|
+
* bir kompozit geçiş yapar (yüzlerce `transition-colors`'ın aynı anda ve
|
|
881
|
+
* farklı sürelerle kayması yerine).
|
|
882
|
+
*
|
|
883
|
+
* `update` HER ZAMAN çağrılır — sadece NASIL çağrıldığı değişir. Geçiş
|
|
884
|
+
* atlanıp `update` doğrudan (senkron) çalıştırılır:
|
|
885
|
+
* - SSR / `document` yok,
|
|
886
|
+
* - tarayıcı `startViewTransition` desteklemiyor,
|
|
887
|
+
* - kullanıcı `prefers-reduced-motion: reduce` istiyor.
|
|
888
|
+
*
|
|
889
|
+
* ```ts
|
|
890
|
+
* transitionMfTheme(() => document.documentElement.classList.toggle('dark'));
|
|
891
|
+
* ```
|
|
892
|
+
*/
|
|
893
|
+
function transitionMfTheme(update) {
|
|
894
|
+
if (typeof document === 'undefined') {
|
|
895
|
+
update();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const reducedMotion = typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
899
|
+
// DOM lib tipi bunu zorunlu sayar, ama Firefox < 144 ve jsdom'da YOK.
|
|
900
|
+
if (typeof document.startViewTransition !== 'function' || reducedMotion) {
|
|
901
|
+
update();
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
document.startViewTransition(update);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const MF_COLOR_MODE_CONFIG = new InjectionToken('mesgflow.color-mode-config', { factory: () => ({ storageKey: 'mf-color-mode', defaultMode: 'system' }) });
|
|
908
|
+
/** `providers: [provideMfColorMode({ storageKey: 'app-theme' })]` */
|
|
909
|
+
function provideMfColorMode(config) {
|
|
910
|
+
return {
|
|
911
|
+
provide: MF_COLOR_MODE_CONFIG,
|
|
912
|
+
useValue: { storageKey: 'mf-color-mode', defaultMode: 'system', ...config },
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
const DARK_QUERY = '(prefers-color-scheme: dark)';
|
|
916
|
+
/**
|
|
917
|
+
* Açık / koyu / sistem modu — kitin TEK kaynağı (`<mf-theme-switcher>` bunu kullanır; kendi
|
|
918
|
+
* anahtarınızla da doğrudan enjekte edebilirsiniz).
|
|
919
|
+
*
|
|
920
|
+
* - `mode`: kullanıcının seçimi (kalıcı, `localStorage`); `resolved`: gerçekte uygulanan
|
|
921
|
+
* `light`/`dark` — `system`'de işletim sistemi tercihini CANLI izler.
|
|
922
|
+
* - Uygulama: kökte `.dark` sınıfı + `color-scheme` (yerel kaydırma çubukları ve form
|
|
923
|
+
* kontrolleri de temaya uyar); değişim `transitionMfTheme` ile yumuşak (azaltılmış harekette
|
|
924
|
+
* anında). Sınıf zaten doğruysa (ilk boyamada `colorModeInitScript` uyguladı) dokunulmaz.
|
|
925
|
+
* - Depolama/`matchMedia` yoksa (gizli pencere, SSR, test) sessizce varsayılana düşer.
|
|
926
|
+
*/
|
|
927
|
+
class MfColorModeService {
|
|
928
|
+
config = inject(MF_COLOR_MODE_CONFIG);
|
|
929
|
+
document = inject(DOCUMENT);
|
|
930
|
+
media = this.document.defaultView?.matchMedia?.(DARK_QUERY) ?? null;
|
|
931
|
+
systemDark = signal(this.media?.matches ?? false, /* @ts-ignore */
|
|
932
|
+
...(ngDevMode ? [{ debugName: "systemDark" }] : /* istanbul ignore next */ []));
|
|
933
|
+
mode = signal(this.readStored(), /* @ts-ignore */
|
|
934
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
935
|
+
resolved = computed(() => {
|
|
936
|
+
const mode = this.mode();
|
|
937
|
+
return mode === 'system' ? (this.systemDark() ? 'dark' : 'light') : mode;
|
|
938
|
+
}, /* @ts-ignore */
|
|
939
|
+
...(ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []));
|
|
940
|
+
constructor() {
|
|
941
|
+
const onChange = (event) => this.systemDark.set(event.matches);
|
|
942
|
+
this.media?.addEventListener?.('change', onChange);
|
|
943
|
+
inject(DestroyRef).onDestroy(() => this.media?.removeEventListener?.('change', onChange));
|
|
944
|
+
effect(() => {
|
|
945
|
+
const mode = this.mode();
|
|
946
|
+
try {
|
|
947
|
+
this.document.defaultView?.localStorage.setItem(this.config.storageKey, mode);
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
// Gizli pencere / engelli depolama — yalnızca bu oturumda.
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
effect(() => {
|
|
954
|
+
const dark = this.resolved() === 'dark';
|
|
955
|
+
const root = this.document.documentElement;
|
|
956
|
+
root.style.colorScheme = dark ? 'dark' : 'light';
|
|
957
|
+
if (root.classList.contains('dark') === dark)
|
|
958
|
+
return;
|
|
959
|
+
transitionMfTheme(() => root.classList.toggle('dark', dark));
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
setMode(mode) {
|
|
963
|
+
this.mode.set(mode);
|
|
964
|
+
}
|
|
965
|
+
/** light → dark → system → light (tek düğmeli değiştirici için). */
|
|
966
|
+
cycle() {
|
|
967
|
+
const order = ['light', 'dark', 'system'];
|
|
968
|
+
this.mode.set(order[(order.indexOf(this.mode()) + 1) % order.length]);
|
|
969
|
+
}
|
|
970
|
+
readStored() {
|
|
971
|
+
try {
|
|
972
|
+
const stored = this.document.defaultView?.localStorage.getItem(this.config.storageKey);
|
|
973
|
+
if (stored === 'light' || stored === 'dark' || stored === 'system')
|
|
974
|
+
return stored;
|
|
975
|
+
}
|
|
976
|
+
catch {
|
|
977
|
+
// Depolama erişilemez.
|
|
978
|
+
}
|
|
979
|
+
return this.config.defaultMode;
|
|
980
|
+
}
|
|
981
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: MfColorModeService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
982
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.6", ngImport: i0, type: MfColorModeService });
|
|
983
|
+
}
|
|
984
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: MfColorModeService, decorators: [{
|
|
985
|
+
type: Service
|
|
986
|
+
}], ctorParameters: () => [] });
|
|
987
|
+
/**
|
|
988
|
+
* `index.html` `<head>`'ine satır içi konacak betik — Angular yüklenmeden ÖNCE `.dark`'ı uygular,
|
|
989
|
+
* koyu modda beyaz flaşı önler. Anahtar ve varsayılan servisle AYNI olmalı.
|
|
990
|
+
*
|
|
991
|
+
* ```html
|
|
992
|
+
* <script>/* colorModeInitScript('app-theme') çıktısı *\/</script>
|
|
993
|
+
* ```
|
|
994
|
+
*/
|
|
995
|
+
function colorModeInitScript(storageKey = 'mf-color-mode', defaultMode = 'system') {
|
|
996
|
+
return (`(function(){try{var m=localStorage.getItem(${JSON.stringify(storageKey)})||${JSON.stringify(defaultMode)};` +
|
|
997
|
+
`var d=m==='dark'||(m==='system'&&matchMedia('${DARK_QUERY}').matches);` +
|
|
998
|
+
`var r=document.documentElement;r.classList.toggle('dark',d);r.style.colorScheme=d?'dark':'light';}catch(e){}})();`);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/*
|
|
1002
|
+
* `@mesgflow/ui-kit/theming` — ikincil giriş noktası. `i18n/tr` gibi BİLEREK
|
|
1003
|
+
* birincil barrel'dan (`projects/ui-kit/index.ts`) re-export EDİLMEZ: tema
|
|
1004
|
+
* özelleştirme her tüketicinin ihtiyacı değil, opt-in kalmalı (bkz.
|
|
1005
|
+
* `docs/tr/mimari.md` "i18n mimarisi"nin aynı gerekçesi).
|
|
1006
|
+
*
|
|
1007
|
+
* `color-math.ts` BİLEREK burada export EDİLMEZ — iç uygulama detayı
|
|
1008
|
+
* (hex<->OKLCH), public API `ramp.ts`/`apply.ts`'in üstünde.
|
|
1009
|
+
*/
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Generated bundle index. Do not edit.
|
|
1013
|
+
*/
|
|
1014
|
+
|
|
1015
|
+
export { MF_COLOR_MODE_CONFIG, MF_NEUTRAL_RAMP_STEPS, MF_RAMP_TONES, MF_ROLE_TOKENS, MF_THEME_JSON_SCHEMA, MfColorModeService, RAMP_STEPS, accentContrastWithWhite, applyMfTheme, colorModeInitScript, createAccentRamp, exportMfThemeCss, fgOnAccent, provideMfColorMode, resetMfTheme, transitionMfTheme, validateMfTheme };
|
|
1016
|
+
//# sourceMappingURL=mesgflow-ui-kit-theming.mjs.map
|