diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 2feaf1dda..74bca7239 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -1,5 +1,5 @@ - + diff --git a/web_src/js/index.js b/web_src/js/index.js index 6c30d14fb..64f435732 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -26,7 +26,7 @@ import {initNotificationsTable, initNotificationCount} from './features/notifica import {initStopwatch} from './features/stopwatch.js'; import {createCodeEditor, createMonaco} from './features/codeeditor.js'; import {svg, svgs} from './svg.js'; -import {stripTags} from './utils.js'; +import {stripTags, mqBinarySearch} from './utils.js'; const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; @@ -2519,6 +2519,19 @@ $(document).ready(async () => { .attr('title', ''); }); + // Undo Safari emoji glitch fix at high enough zoom levels + if (navigator.userAgent.match('Safari')) { + $(window).resize(() => { + const px = mqBinarySearch('width', 0, 4096, 1, 'px'); + const em = mqBinarySearch('width', 0, 1024, 0.01, 'em'); + if (em * 16 * 1.25 - px <= -1) { + $('body').addClass('safari-above125'); + } else { + $('body').removeClass('safari-above125'); + } + }); + } + // Semantic UI modules. $('.dropdown:not(.custom)').dropdown({ fullTextSearch: 'exact' diff --git a/web_src/js/utils.js b/web_src/js/utils.js index fc65644c7..bc8474506 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -28,3 +28,16 @@ export function uniq(arr) { export function stripTags(text) { return text.replace(/<[^>]*>?/gm, ''); } + +// searches the inclusive range [minValue, maxValue]. +// credits: https://matthiasott.com/notes/write-your-media-queries-in-pixels-not-ems +export function mqBinarySearch(feature, minValue, maxValue, step, unit) { + if (maxValue - minValue < step) { + return minValue; + } + const mid = Math.ceil((minValue + maxValue) / 2 / step) * step; + if (matchMedia(`screen and (min-${feature}:${mid}${unit})`).matches) { + return mqBinarySearch(feature, mid, maxValue, step, unit); // feature is >= mid + } + return mqBinarySearch(feature, minValue, mid - step, step, unit); // feature is < mid +} diff --git a/web_src/less/_base.less b/web_src/less/_base.less index 351dbf5da..121a2b206 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -1,10 +1,9 @@ :root { /* documented customizable variables */ - --fonts-proportional: system-ui, -apple-system, "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "Liberation Sans", sans-serif; - --fonts-monospace: "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace; - --fonts-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", "Twemoji Mozilla"; + --fonts-proportional: -apple-system, "Segoe UI", system-ui, "Roboto", "Helvetica Neue", "Arial"; + --fonts-monospace: "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace, var(--fonts-emoji); + --fonts-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla"; /* other variables */ - --fonts-regular: var(--fonts-proportional), var(--fonts-emoji); --border-radius: .28571429rem; --opacity-disabled: .55; --color-primary: #4183c4; @@ -115,36 +114,8 @@ --checkbox-mask-indeterminate: url('data:image/svg+xml;utf8,'); } -:root:lang(ja) { - --fonts-proportional: "Hiragino Kaku Gothic ProN", "Yu Gothic", "Source Han Sans JP", "Noto Sans CJK JP", "Droid Sans Japanese", "Meiryo", "MS PGothic"; -} - -:root:lang(zh-CN) { - --fonts-proportional: "PingFang SC", "Hiragino Sans GB", "Source Han Sans CN", "Source Han Sans SC", "Noto Sans CJK SC", "Microsoft YaHei", "Heiti SC", "SimHei"; -} - -:root:lang(zh-TW) { - --fonts-proportional: "PingFang TC", "Hiragino Sans TC", "Source Han Sans TW", "Source Han Sans TC", "Noto Sans CJK TC", "Microsoft JhengHei", "Heiti TC", "PMingLiU"; -} - -:root:lang(zh-HK) { - --fonts-proportional: "PingFang HK", "Hiragino Sans TC", "Source Han Sans HK", "Source Han Sans TC", "Noto Sans CJK TC", "Microsoft JhengHei", "Heiti TC", "PMingLiU_HKSCS", "PMingLiU"; -} - -:root:lang(ko) { - --fonts-proportional: "Apple SD Gothic Neo", "NanumBarunGothic", "Malgun Gothic", "Gulim", "Dotum", "Nanum Gothic", "Source Han Sans KR", "Noto Sans CJK KR"; -} - -@font-face { - font-family: "Yu Gothic"; - src: local("Yu Gothic Medium"); - font-weight: 400; -} - -@font-face { - font-family: "Yu Gothic"; - src: local("Yu Gothic Bold"); - font-weight: 500; +:root * { + --fonts-regular: var(--fonts-override, var(--fonts-proportional)), "Noto Sans", "Liberation Sans", var(--fonts-emoji), sans-serif; } textarea { @@ -1151,10 +1122,18 @@ footer { } } - .ui.language .menu { - max-height: 500px; - overflow-y: auto; - margin-bottom: 7px; + .ui.language { + .menu { + max-height: 500px; + overflow-y: auto; + margin-bottom: 7px; + } + + .svg { + margin-right: .15em; + vertical-align: top; + margin-top: calc(2em - 16px); + } } .ui { @@ -1885,6 +1864,17 @@ table th[data-sortt-desc] { font-style: normal !important; font-weight: normal !important; vertical-align: -.075em; + + @supports (-webkit-hyphens:none) { + body:not(.safari-above125) & { + font-size: inherit; + vertical-align: inherit; + img { + font-size: 1.25em; + vertical-align: -.225em !important; + } + } + } } .emoji img, diff --git a/web_src/less/_font_i18n.less b/web_src/less/_font_i18n.less new file mode 100644 index 000000000..f92459b6f --- /dev/null +++ b/web_src/less/_font_i18n.less @@ -0,0 +1,130 @@ +/* font i18n */ +:root { + /* customizable localized variables */ + :lang(ja) { + --fonts-override: var(--fonts-default-override-ja); + } + :lang(zh-CN) { + --fonts-override: var(--fonts-default-override-zh-cn); + } + :lang(zh-TW) { + --fonts-override: var(--fonts-default-override-zh-tw); + } + :lang(zh-HK) { + --fonts-override: var(--fonts-default-override-zh-hk); + } + :lang(ko) { + --fonts-override: var(--fonts-default-override-ko); + } +} + +[lang] { + font-family: var(--fonts-regular); +} + +each(@fonts, { + @weights: .gen-weights-all(@value); + @locale: replace(@key, "@", "-"); + .font-face-cjk(~"system-ui@{locale}", @weights[@light], 300); + .font-face-cjk(~"system-ui@{locale}", @weights[@regular], 400); + .font-face-cjk(~"system-ui@{locale}", @weights[@medium], 500); + .font-face-cjk(~"system-ui@{locale}", @weights[@bold], 700); + /* Safari on macOS/iOS */ + @font-face { + font-family: ~"system-ui@{locale}"; + src: local("HelveticaNeue"); + unicode-range: U+A0; + } + /* Other browsers on macOS/iOS */ + @supports not (-webkit-hyphens:none) { + @font-face { + font-family: ~"system-ui@{locale}"; + src: local("HelveticaNeue"); + unicode-range: U+20; + } + } + :root { + /* Special handling for Firefox on Windows/Linux */ + @supports (-moz-appearance:none) { + --fonts-default-override@{locale}: ~"var(--fonts-proportional), system-ui@{locale}"; + } + --fonts-default-override@{locale}: ~"system-ui@{locale}, var(--fonts-proportional)"; + } +}); + +@fonts: { + @ja: + "HiraginoSans-:{W2,W4,W5,W6}", "HiraKakuProN-:{W3,W6}", "Hiragino Kaku Gothic ProN :{W3,W6}", + .shs("JP")[], .shs("J")[], .noto("JP")[], .shs("")[], + /* https://acetaminophen.hatenablog.com/entry/2016/02/15/225009 */ + "Yu Gothic :{Regular,Medium,Bold}", "YuGothic :{Regular,Medium,Bold}", + "Droid Sans Japanese:{}", "Meiryo:{, Bold}", "MS PGothic:{}"; + @zh-cn: + .pingfang("SC")[], + .shs("CN")[], .shs("SC")[], .noto("SC")[], + "HiraginoSansGB-:{W3,W6}", "Hiragino Sans GB :{W3,W6}", + "Microsoft YaHei:{ Light,, Bold}", "Heiti SC :{Light,Medium}", "SimHei:{}"; + @zh-tw: + .pingfang("TC")[], + .shs("TW")[], .shs("TC")[], .noto("TC")[], + "HiraginoSansTC-:{W3,W6}", "Hiragino Sans TC :{W3,W6}", + "Microsoft JhengHei:{ Light,, Bold}", "Heiti TC :{Light,Medium}", "PMingLiU:{}"; + @zh-hk: + .pingfang("HK")[], + .shs("HK")[], .shs("HC")[], .noto("HK")[], .shs("TC")[], .noto("TC")[], + "HiraginoSansTC-:{W3,W6}", "Hiragino Sans TC :{W3,W6}", + "Microsoft JhengHei:{ Light,, Bold}", "Heiti TC :{Light,Medium}", "PMingLiU_HKSCS:{}", "PMingLiU:{}"; + @ko: + "AppleSDGothicNeo-:{Light,Regular,Medium,SemiBold}", + .shs("KR")[], .shs("K")[], .noto("KR")[], + "NanumBarunGothic:{ Light,, Bold}", + "Malgun Gothic:{ Semilight,, Bold}", "Nanum Gothic:{, Bold}", "Dotum:{}"; +} + +.noto(@suffix) { @value: "Noto Sans CJK @{suffix} ", "NotoSansCJK@{suffix}-"; } +.shs(@suffix) { @value: replace("Source Han Sans @{suffix} ", " ", " "), "SourceHanSans@{suffix}-"; } +.pingfang(@suffix) { @value: "PingFang@{suffix}-:{Light,Regular,Medium,Semibold}"; } +.font-face-cjk(@family, @src, @weight) { + @font-face { + font-family: @family; + src: @src; + font-weight: @weight; + unicode-range: ~"U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF, U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????"; + } +} + +.gen-weights(@family) when (isstring(@family)) { + @family-str: replace(@family, ":\{.*\}$", ""); + // apply standard style names if none is given + // should the font have no styles, use :{}, as in "SimHei:{}" + @weights-str: if(@family = @family-str, "Light,Regular,Medium,Bold", replace(@family, ".*:\{(.*)\}$", "$1")); + @lightest: replace(@weights-str, ",.*", ""); + @boldest: replace(@weights-str, ".*,", ""); + @2ndboldest: replace(@weights-str, "(?:.*,|)([^,]*),.*$", "$1"); + @2ndlightest: if(@2ndboldest = @lightest, @lightest, replace(@weights-str, "^.*?,([^,]*).*", "$1")); + + @light: local("@{family-str}@{lightest}"); + @regular: local("@{family-str}@{2ndlightest}"); + @medium: local("@{family-str}@{2ndboldest}"); + @bold: local("@{family-str}@{boldest}"); +} +.gen-weights(@family) when not (isstring(@family)) { + .gen-weights-all(@family); +} +.gen-weights(@family, @last) { + @this: .gen-weights(@family); + + @light: @last[@light], @this[@light]; + @regular: @last[@regular], @this[@regular]; + @medium: @last[@medium], @this[@medium]; + @bold: @last[@bold], @this[@bold]; +} +.gen-weights-all(@family) when not (isstring(@family)) { + .gen-weights-all(@family, length(@family)); +} +.gen-weights-all(@family, 1) when not (isstring(@family)) { + .gen-weights(extract(@family, 1)); +} +.gen-weights-all(@family, @ctr) when not (isstring(@family)) and (@ctr > 1) and (@ctr <= length(@family)) { + .gen-weights(extract(@family, @ctr), .gen-weights-all(@family, @ctr - 1)); +} diff --git a/web_src/less/index.less b/web_src/less/index.less index cd70eedef..b3cbf8abb 100644 --- a/web_src/less/index.less +++ b/web_src/less/index.less @@ -13,6 +13,7 @@ @import "_svg"; @import "_tribute"; +@import "_font_i18n"; @import "_base"; @import "_markdown"; @import "_home";