@ryuzaki13/react-foundation-ui 1.8.11 → 1.8.13
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/dist/chunks/defaultRenderers-u_Vep6Pj.js.map +1 -1
- package/dist/chunks/multi-select-DtmRtX6v.js.map +1 -1
- package/dist/chunks/select-MN6zDPKM.js.map +1 -1
- package/dist/chunks/useODataSelectBase-DR4PrY1j.js.map +1 -1
- package/dist/multi-select/model/useODataMultiSelect.d.ts +1 -1
- package/dist/multi-select/model/useODataMultiSelect.d.ts.map +1 -1
- package/dist/multi-select/ui/MultiSelect.d.ts +1 -1
- package/dist/multi-select/ui/MultiSelect.d.ts.map +1 -1
- package/dist/multi-select/ui/SerializableMultiSelect.d.ts.map +1 -1
- package/dist/multi-select/ui/defaultRenderers.d.ts +1 -1
- package/dist/multi-select/ui/defaultRenderers.d.ts.map +1 -1
- package/dist/select/model/useODataSelect.d.ts +1 -1
- package/dist/select/model/useODataSelect.d.ts.map +1 -1
- package/dist/select/model/useODataSelectBase.d.ts +1 -1
- package/dist/select/model/useODataSelectBase.d.ts.map +1 -1
- package/dist/tree-select/index.js.map +1 -1
- package/dist/tree-select/model/buildODataTreeNodes.d.ts +1 -1
- package/dist/tree-select/model/buildODataTreeNodes.d.ts.map +1 -1
- package/package.json +3 -3
- package/storybook-static/assets/{Badge-BojMuOws.js → Badge-BFhBl7yM.js} +1 -1
- package/storybook-static/assets/{Badge.stories-C9gJRuIO.js → Badge.stories-BEu55X4s.js} +1 -1
- package/storybook-static/assets/{BadgeList-dNEWI7-O.js → BadgeList-mqmnGtRR.js} +1 -1
- package/storybook-static/assets/{Button-Bbt3JzIY.js → Button-CESdnWrO.js} +1 -1
- package/storybook-static/assets/{Button-CrTHXWYk.js → Button-_YPAu9sd.js} +1 -1
- package/storybook-static/assets/{Button.stories-BaYXOBeF.js → Button.stories-B0Y_T17B.js} +1 -1
- package/storybook-static/assets/{CalendarView-Dp0zus1o.js → CalendarView-DLuO2Ywz.js} +1 -1
- package/storybook-static/assets/{CheckBox-BiK9yWNt.js → CheckBox-CGiNmAPT.js} +1 -1
- package/storybook-static/assets/{CheckBox-UzYHaD6S.js → CheckBox-l0pbBm9z.js} +1 -1
- package/storybook-static/assets/{CheckBox.stories-C2BOiWZj.js → CheckBox.stories-ROPAnqh8.js} +1 -1
- package/storybook-static/assets/{Color-6AOWCN3U-C5S4wUlD.js → Color-6AOWCN3U-kPaRN0HS.js} +1 -1
- package/storybook-static/assets/{ContextMenu-D9EZxb7d.js → ContextMenu-Bob4DtRn.js} +1 -1
- package/storybook-static/assets/{ContextMenu-BWQln_tb.js → ContextMenu-DxfUAeJS.js} +1 -1
- package/storybook-static/assets/{ContextMenu.stories-U38zPh9_.js → ContextMenu.stories-DCJmWy5n.js} +1 -1
- package/storybook-static/assets/{CustomOptionButton-COIOrxQn.js → CustomOptionButton-DuEz9CXt.js} +1 -1
- package/storybook-static/assets/DateInput-BQ8SoeLE.js +1 -0
- package/storybook-static/assets/{DateInput-C-entb-r.js → DateInput-DmNLD4Dz.js} +1 -1
- package/storybook-static/assets/{DateInput.stories-NmUXQLDU.js → DateInput.stories-sTpYf7Ob.js} +1 -1
- package/storybook-static/assets/{DateRangePresetSelect-BM1L1KOo.js → DateRangePresetSelect-DRq4eJ2n.js} +1 -1
- package/storybook-static/assets/{DateRangePresetSelect-CAJzxaq0.js → DateRangePresetSelect-fgJfLaLO.js} +1 -1
- package/storybook-static/assets/{DateRangePresetSelect.stories-CC47XLnD.js → DateRangePresetSelect.stories-ib7Qh_BB.js} +1 -1
- package/storybook-static/assets/{Detail-CDluXtts.js → Detail-CJniLn1R.js} +1 -1
- package/storybook-static/assets/{Detail.stories-DjniLC4n.js → Detail.stories-BXjdNaL-.js} +1 -1
- package/storybook-static/assets/{Dialog-bzRSgGU7.js → Dialog-D6a_qzr3.js} +1 -1
- package/storybook-static/assets/{Dialog-bw6cHUsz.js → Dialog-DOYnnXlU.js} +1 -1
- package/storybook-static/assets/{Dialog.stories-C6Rs6s_p.js → Dialog.stories-B1cZLRK3.js} +1 -1
- package/storybook-static/assets/{Disclosure-DelA6FsQ.js → Disclosure-Chh5o_0s.js} +1 -1
- package/storybook-static/assets/{Disclosure.stories-D7yUinyb.js → Disclosure.stories-Cvdvf7V8.js} +1 -1
- package/storybook-static/assets/{DisclosureGroup-DbK7mU_Q.js → DisclosureGroup-CRnQOgVj.js} +1 -1
- package/storybook-static/assets/{DocsRenderer-JROSPFPF-v3z0zVN1.js → DocsRenderer-JROSPFPF-BBiErkeC.js} +1 -1
- package/storybook-static/assets/{DropZone-CI3NW4AC.js → DropZone-Bm5gqdi9.js} +1 -1
- package/storybook-static/assets/{DropZone-Cyy-ttEO.js → DropZone-C4L7JAlq.js} +1 -1
- package/storybook-static/assets/{DropZone.stories-S0HSpGTB.js → DropZone.stories-BhA9q8Qv.js} +1 -1
- package/storybook-static/assets/{DropdownMenu-Ct1bSZsE.js → DropdownMenu-DtZNSUHF.js} +1 -1
- package/storybook-static/assets/{ExpandableActionPanel-Oj3xlasj.js → ExpandableActionPanel-0ugXlJy_.js} +1 -1
- package/storybook-static/assets/{ExpandableActionPanel.stories-Ch7CLkpR.js → ExpandableActionPanel.stories-CLgMlA9k.js} +1 -1
- package/storybook-static/assets/{Flex-UiFm8Tkc.js → Flex-CJlia-8f.js} +1 -1
- package/storybook-static/assets/{Flex.stories-CaDiVlBD.js → Flex.stories-DCvfi1EY.js} +1 -1
- package/storybook-static/assets/{FloatingPopover-CTYV9W7T.js → FloatingPopover-CsnfPiwV.js} +1 -1
- package/storybook-static/assets/{FormattedTableCell-D-ysSR3p.js → FormattedTableCell-Dz5xWVYO.js} +1 -1
- package/storybook-static/assets/{Grid-CigBXPe2.js → Grid-C-KwTRAM.js} +1 -1
- package/storybook-static/assets/{Grid.stories-ay3z9DQZ.js → Grid.stories-CzbqknNR.js} +1 -1
- package/storybook-static/assets/{ImageCropper.stories-Dkce_ugb.js → ImageCropper.stories-DaE-gm-b.js} +1 -1
- package/storybook-static/assets/{ImageRenderer-C9_EmGBW.js → ImageRenderer-IsNCXCrZ.js} +1 -1
- package/storybook-static/assets/{ImageRenderer.stories-C7bXaNBI.js → ImageRenderer.stories-BbsjUEfJ.js} +1 -1
- package/storybook-static/assets/{ImageView.stories-D-ZzGlk8.js → ImageView.stories-DY0UITYz.js} +1 -1
- package/storybook-static/assets/{ImageViewer.stories-ItcRfxyy.js → ImageViewer.stories--9q9b9nv.js} +2 -2
- package/storybook-static/assets/{ImageViewerRuntime-eaupID36.js → ImageViewerRuntime-CNEH3XGN.js} +1 -1
- package/storybook-static/assets/{Input-op4j7Beh.js → Input-DLubbE7P.js} +1 -1
- package/storybook-static/assets/{Input-BUDNR96c.js → Input-Dw2JblYt.js} +1 -1
- package/storybook-static/assets/{Input.stories-DwulvJIg.js → Input.stories-DQeyDULG.js} +1 -1
- package/storybook-static/assets/{InputDateTime-B_cxOp6k.js → InputDateTime-BRzl8Vrt.js} +1 -1
- package/storybook-static/assets/{InputDateTime.stories-BueLF4HC.js → InputDateTime.stories-BJTj5v8J.js} +1 -1
- package/storybook-static/assets/{InputFile-DSg7sugm.js → InputFile-B8UroZji.js} +1 -1
- package/storybook-static/assets/{InputFile-C8z6TPc1.js → InputFile-DPftxjGw.js} +1 -1
- package/storybook-static/assets/{InputFile.stories-C6XSbIua.js → InputFile.stories-C7yyqHqc.js} +1 -1
- package/storybook-static/assets/{InputFiles-DoatLq3q.js → InputFiles-BQVXHVLy.js} +1 -1
- package/storybook-static/assets/{InputFiles-DeZ84Uu_.js → InputFiles-UD_9S7-N.js} +1 -1
- package/storybook-static/assets/{InputFiles.stories-BMbE8SJU.js → InputFiles.stories-DjjJS5pZ.js} +1 -1
- package/storybook-static/assets/{InputFilesList-CsfDDqaJ.js → InputFilesList-BNjijD3Y.js} +1 -1
- package/storybook-static/assets/{InputImage-yJSq98EH.js → InputImage-C-pHr-Sx.js} +1 -1
- package/storybook-static/assets/{InputImage-D_YnB2Tw.js → InputImage-DbcbqIhY.js} +1 -1
- package/storybook-static/assets/{InputImage.stories-nivUG307.js → InputImage.stories-D-TC9r9t.js} +1 -1
- package/storybook-static/assets/{InteractiveComponents-IPkC_Rkg.js → InteractiveComponents-DbhWgM9y.js} +1 -1
- package/storybook-static/assets/{InteractiveComponents.stories-D4VzVKZJ.js → InteractiveComponents.stories-B4mkVSea.js} +1 -1
- package/storybook-static/assets/{LayoutPicker-ZHkMi-LM.js → LayoutPicker-BoV_E3qt.js} +1 -1
- package/storybook-static/assets/{LayoutPicker-aMzPN5Do.js → LayoutPicker-w1EsRpYP.js} +1 -1
- package/storybook-static/assets/{LayoutPicker.stories-DAzpzxn5.js → LayoutPicker.stories--ChGnGxf.js} +1 -1
- package/storybook-static/assets/{Listbox-CR5nXC9J.js → Listbox-CzZRsr6a.js} +1 -1
- package/storybook-static/assets/{Listbox-BmfQOnrb.js → Listbox-qZdTi11b.js} +1 -1
- package/storybook-static/assets/{Listbox.stories-CeOpsKxB.js → Listbox.stories-C9k_1850.js} +1 -1
- package/storybook-static/assets/{Modal-BStp52Zo.js → Modal-DCpul8QM.js} +1 -1
- package/storybook-static/assets/{Modal.stories-CwGm5GYE.js → Modal.stories-Cy0Xd4LF.js} +1 -1
- package/storybook-static/assets/{ModalManagerProvider-B5uUXWF9.js → ModalManagerProvider-BrPVMcHV.js} +1 -1
- package/storybook-static/assets/{MultiSelect-308npb27.js → MultiSelect-BEQdl-F4.js} +1 -1
- package/storybook-static/assets/{MultiSelect-BDs02MZh.js → MultiSelect-Dy4X94E2.js} +1 -1
- package/storybook-static/assets/{MultiSelect.stories-DRi_iTp4.js → MultiSelect.stories-FuzejJmz.js} +1 -1
- package/storybook-static/assets/{NoData-ovaQEE9W.js → NoData-BsQ9EfkA.js} +1 -1
- package/storybook-static/assets/{NotificationsHost-BpeMWhLY.js → NotificationsHost-BSrODvw7.js} +1 -1
- package/storybook-static/assets/NotificationsHost.stories-DeiWspcY.js +29 -0
- package/storybook-static/assets/{ODataMultiSelect-qlft9L9L.js → ODataMultiSelect-BEz9gcd2.js} +1 -1
- package/storybook-static/assets/{ODataMultiSelect.stories-CMPIoPiy.js → ODataMultiSelect.stories-BwMgAkbE.js} +2 -2
- package/storybook-static/assets/{ODataSelect-DKBQArAK.js → ODataSelect-BgC7GHOv.js} +1 -1
- package/storybook-static/assets/{ODataSelect.stories-DxYIhtwe.js → ODataSelect.stories-DEIRAB-s.js} +2 -2
- package/storybook-static/assets/{ODataTreeMultiSelect-DAt6THP3.js → ODataTreeMultiSelect-WyZZuX0_.js} +1 -1
- package/storybook-static/assets/{ODataTreeMultiSelect.stories-CBNJP0mP.js → ODataTreeMultiSelect.stories-B6cxuagy.js} +1 -1
- package/storybook-static/assets/{ODataTreeSelect-Bzzt4CQC.js → ODataTreeSelect-OJGi3-S7.js} +1 -1
- package/storybook-static/assets/{ODataTreeSelect.stories-CWVvuqOt.js → ODataTreeSelect.stories-wGRGErCH.js} +1 -1
- package/storybook-static/assets/{OneStepScroller-BY5lz8qi.js → OneStepScroller-DycQ3RuL.js} +1 -1
- package/storybook-static/assets/{Option-Rw8xwrPg.js → Option-taxbFfjo.js} +1 -1
- package/storybook-static/assets/{OptionButton-Br48w_Zg.js → OptionButton-Q0QaPCII.js} +1 -1
- package/storybook-static/assets/{PeriodSelect-8-wpXz6w.js → PeriodSelect-DUJ2nF3F.js} +1 -1
- package/storybook-static/assets/{PeriodSelect-DxSRwKw-.js → PeriodSelect-DnM7x-uC.js} +1 -1
- package/storybook-static/assets/{PeriodSelect.stories-B-qgUzlS.js → PeriodSelect.stories-Db0bqe3J.js} +1 -1
- package/storybook-static/assets/{PickerTriggerActions-C5xmysFh.js → PickerTriggerActions-CGJd7xmR.js} +1 -1
- package/storybook-static/assets/{Popover-NJwt_vZZ.js → Popover-B6oidz5p.js} +1 -1
- package/storybook-static/assets/{Popover-Ds8PUigp.js → Popover-Tbhf8T81.js} +1 -1
- package/storybook-static/assets/{Popover.stories-CUOFA_Xf.js → Popover.stories-DE1LxZvv.js} +1 -1
- package/storybook-static/assets/{PresetRangeDateInput-DyOpbkON.js → PresetRangeDateInput-CQ5FfmrL.js} +1 -1
- package/storybook-static/assets/{PresetRangeDateInput-Xk0bOy3S.js → PresetRangeDateInput-DfgoR0My.js} +1 -1
- package/storybook-static/assets/{PresetRangeDateInput.stories-CwjUlXj0.js → PresetRangeDateInput.stories-CgoYAYBG.js} +1 -1
- package/storybook-static/assets/{PresetSelect-Ddmr0niY.js → PresetSelect-BPtAknAr.js} +1 -1
- package/storybook-static/assets/README--TpGcM95.js +110 -0
- package/storybook-static/assets/{README-MzcO5Z_P.js → README-4iBHWAP5.js} +1 -1
- package/storybook-static/assets/README-98KIgy5G.js +75 -0
- package/storybook-static/assets/README-B6II8p0W.js +154 -0
- package/storybook-static/assets/{README-CkWglKbS.js → README-B6vU5_7I.js} +1 -1
- package/storybook-static/assets/README-BFJVjjZR.js +325 -0
- package/storybook-static/assets/README-BIT4HlFP.js +441 -0
- package/storybook-static/assets/{README-DqQDgJN-.js → README-BK6Epio1.js} +1 -1
- package/storybook-static/assets/{README-DRnlD7y8.js → README-BRlPB5_k.js} +1 -1
- package/storybook-static/assets/README-BTFpEaXG.js +64 -0
- package/storybook-static/assets/README-BWhXgC_o.js +226 -0
- package/storybook-static/assets/README-BbNHvOoR.js +101 -0
- package/storybook-static/assets/README-BbunUo8b.js +118 -0
- package/storybook-static/assets/README-BmYpfZKr.js +1844 -0
- package/storybook-static/assets/README-Bu5uvhAT.js +230 -0
- package/storybook-static/assets/README-Bx6fvSBE.js +1282 -0
- package/storybook-static/assets/README-Bx6gIGiA.js +69 -0
- package/storybook-static/assets/README-ByhxsX44.js +324 -0
- package/storybook-static/assets/README-Byi1FFpw.js +98 -0
- package/storybook-static/assets/README-C3Is3a3B.js +208 -0
- package/storybook-static/assets/{README-C0BwTe5N.js → README-C6prwAXw.js} +1 -1
- package/storybook-static/assets/{README-BzVni2ve.js → README-C9S83J_G.js} +1 -1
- package/storybook-static/assets/{README-BHw6MVjD.js → README-CGgxP5Zo.js} +1 -1
- package/storybook-static/assets/README-CJ2ZVG2U.js +2124 -0
- package/storybook-static/assets/README-CNGCAm5d.js +95 -0
- package/storybook-static/assets/README-Cb_q420B.js +90 -0
- package/storybook-static/assets/README-CtIDPNJ3.js +186 -0
- package/storybook-static/assets/README-CzO3Dchr.js +64 -0
- package/storybook-static/assets/README-D0rzQthv.js +264 -0
- package/storybook-static/assets/README-DEQu7djz.js +1704 -0
- package/storybook-static/assets/README-DFihT_sm.js +296 -0
- package/storybook-static/assets/{README-CTIBG4zH.js → README-DMiWsyv_.js} +1 -1
- package/storybook-static/assets/README-DOx8lXXk.js +82 -0
- package/storybook-static/assets/{README-DzTc6XmY.js → README-DPhDjr43.js} +1 -1
- package/storybook-static/assets/{README-Dki94Nf2.js → README-D_NqWPa9.js} +1 -1
- package/storybook-static/assets/README-DhJ5Rb1w.js +355 -0
- package/storybook-static/assets/README-DlwhWCvQ.js +1146 -0
- package/storybook-static/assets/README-Dxi73a2Q.js +740 -0
- package/storybook-static/assets/README-Dycov6NP.js +583 -0
- package/storybook-static/assets/README-GTBkSG4_.js +3591 -0
- package/storybook-static/assets/README-Gfd9tLTw.js +672 -0
- package/storybook-static/assets/{README-B4zYJg6u.js → README-Kfawetke.js} +1 -1
- package/storybook-static/assets/{README-BzVHwvgo.js → README-UDBM7ZMT.js} +1 -1
- package/storybook-static/assets/{README-D-XjRtAE.js → README-VTWQxLm_.js} +1 -1
- package/storybook-static/assets/README-YHNWrTZv.js +400 -0
- package/storybook-static/assets/README-_0Otw0eD.js +448 -0
- package/storybook-static/assets/README-aw2Xu9xu.js +158 -0
- package/storybook-static/assets/README-eg1emlll.js +192 -0
- package/storybook-static/assets/README-iqQHdhZ4.js +416 -0
- package/storybook-static/assets/{README-BdPlV5nA.js → README-llxZAtCj.js} +1 -1
- package/storybook-static/assets/README-wFnEIGl4.js +95 -0
- package/storybook-static/assets/{README-Dx3cpFMr.js → README-webB1yH0.js} +1 -1
- package/storybook-static/assets/README-wtreGRCe.js +208 -0
- package/storybook-static/assets/{RadioButton-DWbkCY8u.js → RadioButton-CNOLDk_o.js} +1 -1
- package/storybook-static/assets/{RadioButton-oSvQ-gQh.js → RadioButton-Ce05_q7s.js} +1 -1
- package/storybook-static/assets/{RadioButton.stories-GNQ9XJZT.js → RadioButton.stories-CPd4lTl0.js} +1 -1
- package/storybook-static/assets/{RadioGroup-CvXjGXcV.js → RadioGroup-BtfD5Szd.js} +1 -1
- package/storybook-static/assets/{RadioGroup-eBYggurL.js → RadioGroup-Ccm7G-dv.js} +1 -1
- package/storybook-static/assets/{RadioGroup.stories-C77j_cPv.js → RadioGroup.stories-D-RRPHwi.js} +1 -1
- package/storybook-static/assets/{Scrollable-De6SdhI3.js → Scrollable-DvdBIeig.js} +1 -1
- package/storybook-static/assets/{Select-DQMoVjn4.js → Select-8PV9PBKF.js} +1 -1
- package/storybook-static/assets/{Select-Bwc0bEpt.js → Select-G04B41FM.js} +1 -1
- package/storybook-static/assets/{Select.stories-BSIJKJnM.js → Select.stories-Cf6CBhV8.js} +1 -1
- package/storybook-static/assets/{SerializableSelect-B73irNFR.js → SerializableSelect-CdQdHIVp.js} +1 -1
- package/storybook-static/assets/{SerializableSelect.stories-Z9q6g5v2.js → SerializableSelect.stories-0AKUGpbS.js} +1 -1
- package/storybook-static/assets/{SingleDateTimeInput-Dq9LcjnS.js → SingleDateTimeInput-DWT1ORhL.js} +1 -1
- package/storybook-static/assets/{SingleDateTimeInput-BHl6srqA.js → SingleDateTimeInput-DattLt6w.js} +1 -1
- package/storybook-static/assets/SingleDateTimeInput.stories-BLJV3jzV.js +48 -0
- package/storybook-static/assets/{Slider-CvM0N_kc.js → Slider-DKEI4cDd.js} +1 -1
- package/storybook-static/assets/{Slider-BiyKexKa.js → Slider-Dci-mJt_.js} +1 -1
- package/storybook-static/assets/{Slider.stories-CdHAMDmY.js → Slider.stories-Dp7oQueF.js} +1 -1
- package/storybook-static/assets/{SliderInput-DqHotbhB.js → SliderInput-BGPQmlgY.js} +1 -1
- package/storybook-static/assets/{SliderInput-Bx_oHUCV.js → SliderInput-BhjqGOKt.js} +1 -1
- package/storybook-static/assets/{SliderInput.stories-R5yDhghM.js → SliderInput.stories-DXNhRU3X.js} +1 -1
- package/storybook-static/assets/{Sortable-D7pjTHO0.js → Sortable-CMd9l1Gd.js} +1 -1
- package/storybook-static/assets/{Sortable.stories-CVLG6pSH.js → Sortable.stories-BN1cFEFJ.js} +1 -1
- package/storybook-static/assets/{Splitter-B1Du6XI2.js → Splitter-GL65TlI9.js} +1 -1
- package/storybook-static/assets/{Splitter.stories-D-FRLMCc.js → Splitter.stories-q5PlI6rB.js} +1 -1
- package/storybook-static/assets/{StateSelect-zl-yZibY.js → StateSelect-Byik5-iZ.js} +1 -1
- package/storybook-static/assets/{StateSelect-BJdUEUBc.js → StateSelect-CT0JF2V6.js} +1 -1
- package/storybook-static/assets/{StateSelect.stories-DgidR5zR.js → StateSelect.stories-pPEPmgnN.js} +1 -1
- package/storybook-static/assets/{StringToggle.stories-IucGrtIi.js → StringToggle.stories-BQlFmCE7.js} +1 -1
- package/storybook-static/assets/{Switch-BJrn2lIN.js → Switch--xGPiYk-.js} +1 -1
- package/storybook-static/assets/{Switch-5dCshvVo.js → Switch-DhaLI9CG.js} +1 -1
- package/storybook-static/assets/{Switch.stories-Dp_l3_JL.js → Switch.stories-5DisgIyj.js} +1 -1
- package/storybook-static/assets/{Table-BFAEcUde.js → Table-Bf6MicdG.js} +1 -1
- package/storybook-static/assets/{Table.stories-CohejRQv.js → Table.stories-CS7rQgj9.js} +2 -2
- package/storybook-static/assets/{Tabs-CI32QiHf.js → Tabs-B4y4y8Ap.js} +1 -1
- package/storybook-static/assets/{Tabs.stories-Cr9d9ipA.js → Tabs.stories-C0SpdGkh.js} +1 -1
- package/storybook-static/assets/{TabsBox-BlDOUDMF.js → TabsBox-C5LLLhnj.js} +1 -1
- package/storybook-static/assets/{TagInput-YZ7eSv8W.js → TagInput-D4BpuCoH.js} +1 -1
- package/storybook-static/assets/{TagInput.stories-DrOvsiI5.js → TagInput.stories-rn2wtZP_.js} +1 -1
- package/storybook-static/assets/{Text-DK4jN26J.js → Text-BhBt9e7C.js} +1 -1
- package/storybook-static/assets/{Textarea-CFh5f_lC.js → Textarea-B-Hz7ujq.js} +1 -1
- package/storybook-static/assets/{Textarea-DxPuWBZj.js → Textarea-BHHxY5sd.js} +1 -1
- package/storybook-static/assets/{Textarea.stories-CUeOGKYc.js → Textarea.stories-CZ2kWz20.js} +1 -1
- package/storybook-static/assets/{TimePanel-BUhFtwlb.js → TimePanel-Cg8fgFsn.js} +1 -1
- package/storybook-static/assets/{TimePanel.stories-CpXpMBDN.js → TimePanel.stories-BxdwtK2e.js} +1 -1
- package/storybook-static/assets/{Toggle-l2AvDbRn.js → Toggle-BWwfk9fi.js} +1 -1
- package/storybook-static/assets/{Toggle-BtApzBNH.js → Toggle-Ce9rbjBc.js} +1 -1
- package/storybook-static/assets/{Toggle.stories-Ca8otmAa.js → Toggle.stories-0fzcmzKy.js} +1 -1
- package/storybook-static/assets/{TreeMultiSelect-C9QDV63L.js → TreeMultiSelect-B06Y0lIc.js} +1 -1
- package/storybook-static/assets/{TreeMultiSelect-nXC4ZB5b.js → TreeMultiSelect-CrAdR_jM.js} +1 -1
- package/storybook-static/assets/{TreeMultiSelect.stories-6STRGJbF.js → TreeMultiSelect.stories-CAVoj56E.js} +1 -1
- package/storybook-static/assets/{TreePickerBase-Cq7fSw3x.js → TreePickerBase-qU2xPlar.js} +1 -1
- package/storybook-static/assets/{TreeSelect-CITef9rp.js → TreeSelect-BIHydJFy.js} +1 -1
- package/storybook-static/assets/{TreeSelect-rMNyu_KT.js → TreeSelect-BKnWu0C7.js} +1 -1
- package/storybook-static/assets/{TreeSelect.stories-B-Emz9yW.js → TreeSelect.stories-Dp_8ybAf.js} +1 -1
- package/storybook-static/assets/{TreeTable-CLiP0td3.js → TreeTable-pVXjU9dG.js} +1 -1
- package/storybook-static/assets/{TreeTable.stories-DJb9qt8k.js → TreeTable.stories-CV_8Ui6R.js} +2 -2
- package/storybook-static/assets/{blocks-DndFRxxW.js → blocks-BY40djCc.js} +3 -3
- package/storybook-static/assets/{check-CLkTJVXe.js → check-CVkPTDzp.js} +1 -1
- package/storybook-static/assets/{chevron-down-miCejSEn.js → chevron-down-BtNO4Vhe.js} +1 -1
- package/storybook-static/assets/{chevron-left-DhQIPej2.js → chevron-left-kceSK3sZ.js} +1 -1
- package/storybook-static/assets/{chevron-right-DfBBx-fr.js → chevron-right-BA_LO2El.js} +1 -1
- package/storybook-static/assets/{chunk-W22LQPXL-IdwKKJUw.js → chunk-W22LQPXL-yJiFPv6e.js} +1 -1
- package/storybook-static/assets/{circle-alert-DOmVf8am.js → circle-alert-B2m4a1zz.js} +1 -1
- package/storybook-static/assets/{circle-check-e7eSQ2VD.js → circle-check-Cfjhotlm.js} +1 -1
- package/storybook-static/assets/{components-Bm4Cw8Ap.js → components-D6yV8PLX.js} +3 -3
- package/storybook-static/assets/{contextMenu-DpIKeJju.js → contextMenu-B0pQBIW0.js} +1 -1
- package/storybook-static/assets/{defaultRenderers-m21aWJXz.js → defaultRenderers-DfmnIuod.js} +1 -1
- package/storybook-static/assets/{download-eAKjms6q.js → download-Lba8elK9.js} +1 -1
- package/storybook-static/assets/{floating-ui.react-DmilONq6.js → floating-ui.react-BFNvfMpU.js} +1 -1
- package/storybook-static/assets/formatters-D33Uh3si-CxrG0jsB.js +1 -0
- package/storybook-static/assets/foundationApi-DZhnQ2cC.js +59 -0
- package/storybook-static/assets/foundationLib-CoVTo22U.js +30 -0
- package/storybook-static/assets/{hooks-RI5_2AXI.js → hooks-Cc6eXN6U.js} +1 -1
- package/storybook-static/assets/iframe-BXM-JBuH.js +1 -0
- package/storybook-static/assets/{iframe-Sr0umHM6.js → iframe-o7sH2IXC.js} +4 -4
- package/storybook-static/assets/{info-DhJTzwcL.js → info-BpsVP_dM.js} +1 -1
- package/storybook-static/assets/odata-CXsbxsRY.js +1 -0
- package/storybook-static/assets/odata-service-CWkHASdw-DlmSiioz.js +3 -0
- package/storybook-static/assets/{odataStoryFixtures-lgnWIUp7.js → odataStoryFixtures-Dj_bzRqh.js} +1 -1
- package/storybook-static/assets/{react-YFbUKK0m.js → react-DeJ0JTHb.js} +1 -1
- package/storybook-static/assets/{refresh-cw-C7WnxX9M.js → refresh-cw-BvGyjEXe.js} +1 -1
- package/storybook-static/assets/{settings-2-0NVvyKQl.js → settings-2-BdXxnHos.js} +1 -1
- package/storybook-static/assets/{sortable-DDGG-Zfs.js → sortable-XkyN-9r5.js} +1 -1
- package/storybook-static/assets/{syntaxhighlighter-QEQV7K2Y-CxWm8nc8.js → syntaxhighlighter-QEQV7K2Y-Cafn3LV1.js} +1 -1
- package/storybook-static/assets/{trash-2-BnceCuhg.js → trash-2-uPH9-fND.js} +1 -1
- package/storybook-static/assets/{triangle-alert-MPi3GVgt.js → triangle-alert-tT01vSMw.js} +1 -1
- package/storybook-static/assets/{useODataSelectBase-hmjK6chS.js → useODataSelectBase-B2LlOwPZ.js} +1 -1
- package/storybook-static/assets/useODataTreeData-CU7Rqcb9.js +1 -0
- package/storybook-static/assets/{usePickerQuery-CtsGhieD.js → usePickerQuery-D8GHQ6KR.js} +1 -1
- package/storybook-static/assets/{usePickerTriggerController-EaI8W2G0.js → usePickerTriggerController-CcK1jmDy.js} +1 -1
- package/storybook-static/assets/{x-CqPSvWfo.js → x-BlAURbAH.js} +1 -1
- package/storybook-static/iframe.html +2 -2
- package/storybook-static/index.json +1 -1
- package/storybook-static/project.json +1 -1
- package/storybook-static/assets/DateInput-05Iy69_a.js +0 -1
- package/storybook-static/assets/NotificationsHost.stories-CgbFboVD.js +0 -29
- package/storybook-static/assets/SingleDateTimeInput.stories-C2RuBRAF.js +0 -48
- package/storybook-static/assets/async-SjiCjIjO.js +0 -625
- package/storybook-static/assets/formatters-CBgrBeSA-CqB1pZxS.js +0 -1
- package/storybook-static/assets/foundationLib-DRrox3tN.js +0 -25
- package/storybook-static/assets/iframe-DQo-3eW2.js +0 -1
- package/storybook-static/assets/odata-DhISMZkE.js +0 -1
- package/storybook-static/assets/odata-service-B6TxBSLm-Bq8Ya67E.js +0 -3
- package/storybook-static/assets/odataFetchFn-Dv6Xi5CI.js +0 -763
- package/storybook-static/assets/persisted-CUFiw53N.js +0 -600
- package/storybook-static/assets/useODataTreeData-CKuavbWc.js +0 -1
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
import{n as e}from"./rolldown-runtime-DkW27tQK.js";import{n as t}from"./iframe-o7sH2IXC.js";import{i as n,r}from"./react-Bl2r1tuC.js";import{l as i,m as a}from"./blocks-BY40djCc.js";function o(e){let t={a:`a`,code:`code`,h1:`h1`,h2:`h2`,h3:`h3`,li:`li`,ol:`ol`,p:`p`,pre:`pre`,table:`table`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,...n(),...e.components};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(i,{title:`Foundation Lib/API/Media`}),`
|
|
2
|
+
`,(0,c.jsxs)(t.h1,{id:`адаптивность-и-media-queries-через-ryuzaki13react-foundation-libmedia`,children:[`Адаптивность и media queries через `,(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/media`})]}),`
|
|
3
|
+
`,(0,c.jsxs)(t.p,{children:[`Модуль `,(0,c.jsx)(t.code,{children:`media`}),` содержит общий низкоуровневый контракт адаптивности для React-приложений:`]}),`
|
|
4
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
5
|
+
`,(0,c.jsxs)(t.li,{children:[`проектные брейкпоинты `,(0,c.jsx)(t.code,{children:`mobile`}),`, `,(0,c.jsx)(t.code,{children:`tablet`}),` и `,(0,c.jsx)(t.code,{children:`laptop`}),`;`]}),`
|
|
6
|
+
`,(0,c.jsx)(t.li,{children:`React hook для произвольного CSS media query;`}),`
|
|
7
|
+
`,(0,c.jsx)(t.li,{children:`React hook, который сообщает текущий проектный брейкпоинт;`}),`
|
|
8
|
+
`,(0,c.jsxs)(t.li,{children:[`типы и функции для responsive-значений вида `,(0,c.jsx)(t.code,{children:`{ mobile, tablet, laptop }`}),`;`]}),`
|
|
9
|
+
`,(0,c.jsxs)(t.li,{children:[`чтение вычисленных `,(0,c.jsx)(t.code,{children:`font-size`}),` и `,(0,c.jsx)(t.code,{children:`line-height`}),` из DOM;`]}),`
|
|
10
|
+
`,(0,c.jsxs)(t.li,{children:[`преобразование количества пикселей относительно размера `,(0,c.jsx)(t.code,{children:`em`}),`;`]}),`
|
|
11
|
+
`,(0,c.jsx)(t.li,{children:`расчёт проектной высоты control.`}),`
|
|
12
|
+
`]}),`
|
|
13
|
+
`,(0,c.jsx)(t.p,{children:`Документ рассчитан на разработчика, который использует опубликованный пакет без доступа к его исходному коду. Поэтому ниже отдельно объясняются базовые понятия CSS и React, точные значения, fallback-порядок, SSR, hydration, подписки, cleanup, ограничения и все публичные exports.`}),`
|
|
14
|
+
`,(0,c.jsx)(t.h2,{id:`содержание`,children:`Содержание`}),`
|
|
15
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
16
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0-%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D0%B8`,children:`Граница ответственности`})}),`
|
|
17
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D0%B8-%D0%B8%D0%BC%D0%BF%D0%BE%D1%80%D1%82`,children:`Установка и импорт`})}),`
|
|
18
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5-%D0%BF%D0%BE%D0%BD%D1%8F%D1%82%D0%B8%D1%8F`,children:`Минимальные понятия`})}),`
|
|
19
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%BA%D0%B0%D0%BA-%D0%B2%D1%8B%D0%B1%D1%80%D0%B0%D1%82%D1%8C-api`,children:`Как выбрать API`})}),`
|
|
20
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%BD%D1%8B%D0%B5-%D0%B1%D1%80%D0%B5%D0%B9%D0%BA%D0%BF%D0%BE%D0%B8%D0%BD%D1%82%D1%8B`,children:`Проектные брейкпоинты`})}),`
|
|
21
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#usemediaquery`,children:(0,c.jsx)(t.code,{children:`useMediaQuery`})})}),`
|
|
22
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#usematchmedia`,children:(0,c.jsx)(t.code,{children:`useMatchMedia`})})}),`
|
|
23
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#responsive-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F`,children:`Responsive-значения`})}),`
|
|
24
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#isresponsivemap`,children:(0,c.jsx)(t.code,{children:`isResponsiveMap`})})}),`
|
|
25
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#resolveresponsivevalue`,children:(0,c.jsx)(t.code,{children:`resolveResponsiveValue`})})}),`
|
|
26
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#resolveprops`,children:(0,c.jsx)(t.code,{children:`resolveProps`})})}),`
|
|
27
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%BF%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9-react-%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80`,children:`Полный React-пример`})}),`
|
|
28
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#dom-%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D0%BA%D0%B8`,children:`DOM-метрики`})}),`
|
|
29
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#pxtoem`,children:(0,c.jsx)(t.code,{children:`pxToEm`})})}),`
|
|
30
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#css-%D0%B8%D0%BB%D0%B8-javascript`,children:`CSS или JavaScript`})}),`
|
|
31
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#ssr-%D0%B8-hydration`,children:`SSR и hydration`})}),`
|
|
32
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF%D0%BD%D0%BE%D1%81%D1%82%D1%8C`,children:`Доступность`})}),`
|
|
33
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%BF%D1%80%D0%BE%D0%B8%D0%B7%D0%B2%D0%BE%D0%B4%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D1%8C-%D0%B8-lifecycle`,children:`Производительность и lifecycle`})}),`
|
|
34
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5`,children:`Тестирование`})}),`
|
|
35
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D1%87%D0%B0%D1%81%D1%82%D1%8B%D0%B5-%D0%BE%D1%88%D0%B8%D0%B1%D0%BA%D0%B8`,children:`Частые ошибки`})}),`
|
|
36
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#api-%D1%81%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D0%B8%D0%BA`,children:`API-справочник`})}),`
|
|
37
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`#%D0%B2%D0%BE%D0%BF%D1%80%D0%BE%D1%81%D1%8B-%D0%B8-%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%8B`,children:`Вопросы и ответы`})}),`
|
|
38
|
+
`]}),`
|
|
39
|
+
`,(0,c.jsx)(t.h2,{id:`граница-ответственности`,children:`Граница ответственности`}),`
|
|
40
|
+
`,(0,c.jsx)(t.h3,{id:`что-делает-модуль`,children:`Что делает модуль`}),`
|
|
41
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
42
|
+
`,(0,c.jsx)(t.li,{children:`хранит единый набор project breakpoints;`}),`
|
|
43
|
+
`,(0,c.jsxs)(t.li,{children:[`преобразует breakpoints из `,(0,c.jsx)(t.code,{children:`em`}),` в browser media queries;`]}),`
|
|
44
|
+
`,(0,c.jsx)(t.li,{children:`подписывает React-компонент на изменение media query;`}),`
|
|
45
|
+
`,(0,c.jsxs)(t.li,{children:[`предоставляет стабильный snapshot через React `,(0,c.jsx)(t.code,{children:`useSyncExternalStore`}),`;`]}),`
|
|
46
|
+
`,(0,c.jsx)(t.li,{children:`безопасно отдаёт заранее определённый snapshot во время server render;`}),`
|
|
47
|
+
`,(0,c.jsx)(t.li,{children:`выбирает значение для текущего брейкпоинта с документированным fallback;`}),`
|
|
48
|
+
`,(0,c.jsx)(t.li,{children:`поверхностно разрешает выбранные responsive props;`}),`
|
|
49
|
+
`,(0,c.jsx)(t.li,{children:`читает несколько вычисленных CSS-метрик в браузере;`}),`
|
|
50
|
+
`,(0,c.jsx)(t.li,{children:`освобождает media-query listeners и DOM observers после ухода последнего подписчика.`}),`
|
|
51
|
+
`]}),`
|
|
52
|
+
`,(0,c.jsx)(t.h3,{id:`чего-модуль-не-делает`,children:`Чего модуль не делает`}),`
|
|
53
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
54
|
+
`,(0,c.jsx)(t.li,{children:`не создаёт готовые React-компоненты layout;`}),`
|
|
55
|
+
`,(0,c.jsx)(t.li,{children:`не генерирует CSS-классы и CSS custom properties;`}),`
|
|
56
|
+
`,(0,c.jsxs)(t.li,{children:[`не заменяет CSS `,(0,c.jsx)(t.code,{children:`@media`}),` и container queries;`]}),`
|
|
57
|
+
`,(0,c.jsxs)(t.li,{children:[`не определяет ориентацию, hover, pointer, color scheme или motion автоматически — для этого передайте собственный query в `,(0,c.jsx)(t.code,{children:`useMediaQuery`}),`;`]}),`
|
|
58
|
+
`,(0,c.jsxs)(t.li,{children:[`не предоставляет брейкпоинт `,(0,c.jsx)(t.code,{children:`desktop`}),`;`]}),`
|
|
59
|
+
`,(0,c.jsx)(t.li,{children:`не валидирует строку произвольного media query;`}),`
|
|
60
|
+
`,(0,c.jsx)(t.li,{children:`не рекурсивно обходит вложенные объекты;`}),`
|
|
61
|
+
`,(0,c.jsx)(t.li,{children:`не измеряет ширину конкретного компонента или контейнера;`}),`
|
|
62
|
+
`,(0,c.jsx)(t.li,{children:`не предоставляет polyfill для browser APIs;`}),`
|
|
63
|
+
`,(0,c.jsx)(t.li,{children:`не гарантирует одинаковую разметку для разных viewport на сервере;`}),`
|
|
64
|
+
`,(0,c.jsx)(t.li,{children:`не является дизайн-системой.`}),`
|
|
65
|
+
`]}),`
|
|
66
|
+
`,(0,c.jsx)(t.h3,{id:`соседние-владельцы`,children:`Соседние владельцы`}),`
|
|
67
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Задача`}),(0,c.jsx)(t.th,{children:`Предпочтительный владелец`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Готовые `,(0,c.jsx)(t.code,{children:`Flex`}),`, `,(0,c.jsx)(t.code,{children:`Grid`}),` и responsive UI primitives`]}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-ui/<module>`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Общие media-query hooks и responsive resolver`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/media`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Наблюдение за размером конкретного DOM-элемента`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/dom`}),` или container query`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Touch/coarse-pointer capability`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`useIsTouchDevice`}),` из `,(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/dom`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Debounce/throttle пользовательских callbacks`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/hooks`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Бизнес-решение «что показывать пользователю»`}),(0,c.jsx)(t.td,{children:`entity/feature/widget host-приложения`})]})]})]}),`
|
|
68
|
+
`,(0,c.jsx)(t.p,{children:`Если задача решается только стилями, сначала используйте CSS. JavaScript hook нужен, когда media query действительно влияет на вычисления, выбор поведения, параметры сторонней библиотеки или внешний store.`}),`
|
|
69
|
+
`,(0,c.jsx)(t.h2,{id:`установка-и-импорт`,children:`Установка и импорт`}),`
|
|
70
|
+
`,(0,c.jsx)(t.p,{children:`Установите пакет:`}),`
|
|
71
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-bash`,children:`npm install @ryuzaki13/react-foundation-lib
|
|
72
|
+
`})}),`
|
|
73
|
+
`,(0,c.jsx)(t.p,{children:`Entrypoint экспортирует React hooks, поэтому host-приложение должно предоставить совместимую peer dependency:`}),`
|
|
74
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-bash`,children:`npm install react
|
|
75
|
+
`})}),`
|
|
76
|
+
`,(0,c.jsxs)(t.p,{children:[`Текущий контракт пакета требует React `,(0,c.jsx)(t.code,{children:`>=19.2.0 <20.0.0`}),`.`]}),`
|
|
77
|
+
`,(0,c.jsxs)(t.p,{children:[`Импортируйте runtime API только из опубликованного subpath `,(0,c.jsx)(t.code,{children:`/media`}),`:`]}),`
|
|
78
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import {
|
|
79
|
+
BREAKPOINTS_EM,
|
|
80
|
+
getControlHeight,
|
|
81
|
+
getCurrentFontSize,
|
|
82
|
+
getCurrentLineHeight,
|
|
83
|
+
isResponsiveMap,
|
|
84
|
+
pxToEm,
|
|
85
|
+
resolveProps,
|
|
86
|
+
resolveResponsiveValue,
|
|
87
|
+
useMatchMedia,
|
|
88
|
+
useMediaQuery
|
|
89
|
+
} from "@ryuzaki13/react-foundation-lib/media";
|
|
90
|
+
`})}),`
|
|
91
|
+
`,(0,c.jsxs)(t.p,{children:[`TypeScript-типы импортируйте через `,(0,c.jsx)(t.code,{children:`import type`}),`:`]}),`
|
|
92
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import type {
|
|
93
|
+
Breakpoint,
|
|
94
|
+
MediaMatches,
|
|
95
|
+
ResolvedProps,
|
|
96
|
+
ResponsiveMap,
|
|
97
|
+
ResponsiveValue,
|
|
98
|
+
UnwrapResponsive
|
|
99
|
+
} from "@ryuzaki13/react-foundation-lib/media";
|
|
100
|
+
`})}),`
|
|
101
|
+
`,(0,c.jsx)(t.p,{children:`Корневой и глубокий импорты не являются публичным контрактом:`}),`
|
|
102
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Неправильно: корневой entrypoint не опубликован.
|
|
103
|
+
import { useMatchMedia } from "@ryuzaki13/react-foundation-lib";
|
|
104
|
+
|
|
105
|
+
// Неправильно: внутреннее расположение файла может измениться.
|
|
106
|
+
import { useMatchMedia } from "@ryuzaki13/react-foundation-lib/src/media/useMatchMedia";
|
|
107
|
+
|
|
108
|
+
// Правильно.
|
|
109
|
+
import { useMatchMedia } from "@ryuzaki13/react-foundation-lib/media";
|
|
110
|
+
`})}),`
|
|
111
|
+
`,(0,c.jsx)(t.p,{children:`Пакет поставляется как ESM.`}),`
|
|
112
|
+
`,(0,c.jsx)(t.h2,{id:`минимальные-понятия`,children:`Минимальные понятия`}),`
|
|
113
|
+
`,(0,c.jsx)(t.h3,{id:`viewport`,children:`Viewport`}),`
|
|
114
|
+
`,(0,c.jsxs)(t.p,{children:[`Viewport — видимая область страницы в браузере. Media query `,(0,c.jsx)(t.code,{children:`width`}),` сравнивает условие с шириной viewport, а не с шириной конкретной карточки или `,(0,c.jsx)(t.code,{children:`div`}),`.`]}),`
|
|
115
|
+
`,(0,c.jsx)(t.p,{children:`Если нужно реагировать на ширину отдельного контейнера, обычный viewport media query не подходит. Рассмотрите CSS container queries или измерение конкретного элемента.`}),`
|
|
116
|
+
`,(0,c.jsx)(t.h3,{id:`css-pixel`,children:`CSS pixel`}),`
|
|
117
|
+
`,(0,c.jsxs)(t.p,{children:[`CSS pixel — логическая единица браузерной раскладки. Она не обязана совпадать с одним физическим пикселем экрана. Значение `,(0,c.jsx)(t.code,{children:`window.innerWidth`}),` и границы media query выражаются в CSS pixels.`]}),`
|
|
118
|
+
`,(0,c.jsx)(t.h3,{id:`em`,children:(0,c.jsx)(t.code,{children:`em`})}),`
|
|
119
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`em`}),` — относительная CSS-единица. В этом модуле константы брейкпоинтов хранятся в `,(0,c.jsx)(t.code,{children:`em`}),`, а `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),` перед созданием query умножает их на текущий вычисленный `,(0,c.jsx)(t.code,{children:`font-size`}),` корневого элемента.`]}),`
|
|
120
|
+
`,(0,c.jsxs)(t.p,{children:[`При корневом `,(0,c.jsx)(t.code,{children:`font-size: 16px`}),`:`]}),`
|
|
121
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`48em × 16px = 768px
|
|
122
|
+
72em × 16px = 1152px
|
|
123
|
+
`})}),`
|
|
124
|
+
`,(0,c.jsxs)(t.p,{children:[`При корневом `,(0,c.jsx)(t.code,{children:`font-size: 20px`}),`:`]}),`
|
|
125
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`48em × 20px = 960px
|
|
126
|
+
72em × 20px = 1440px
|
|
127
|
+
`})}),`
|
|
128
|
+
`,(0,c.jsx)(t.p,{children:`Это осознанно связывает адаптивность с настройкой базового размера текста.`}),`
|
|
129
|
+
`,(0,c.jsx)(t.h3,{id:`media-query`,children:`Media query`}),`
|
|
130
|
+
`,(0,c.jsx)(t.p,{children:`Media query — условие среды отображения:`}),`
|
|
131
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-css`,children:`@media (width >= 768px) {
|
|
132
|
+
/* Стили для достаточно широкого viewport. */
|
|
133
|
+
}
|
|
134
|
+
`})}),`
|
|
135
|
+
`,(0,c.jsxs)(t.p,{children:[`JavaScript получает результат условия через `,(0,c.jsx)(t.code,{children:`window.matchMedia`}),`:`]}),`
|
|
136
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const result = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
137
|
+
|
|
138
|
+
console.log(result.matches);
|
|
139
|
+
`})}),`
|
|
140
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`result.matches`}),` — boolean: `,(0,c.jsx)(t.code,{children:`true`}),`, если условие сейчас выполняется, иначе `,(0,c.jsx)(t.code,{children:`false`}),`.`]}),`
|
|
141
|
+
`,(0,c.jsx)(t.h3,{id:`react-hook`,children:`React hook`}),`
|
|
142
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`useMediaQuery`}),` и `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),` являются React hooks. Вызывайте их:`]}),`
|
|
143
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
144
|
+
`,(0,c.jsx)(t.li,{children:`на верхнем уровне function component;`}),`
|
|
145
|
+
`,(0,c.jsx)(t.li,{children:`на верхнем уровне другого custom hook;`}),`
|
|
146
|
+
`,(0,c.jsx)(t.li,{children:`всегда в одинаковом порядке.`}),`
|
|
147
|
+
`]}),`
|
|
148
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`// Неправильно.
|
|
149
|
+
if (enabled) {
|
|
150
|
+
const isMobile = useMediaQuery("(width < 768px)");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Правильно.
|
|
154
|
+
const isMobile = useMediaQuery("(width < 768px)");
|
|
155
|
+
|
|
156
|
+
if (!enabled) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
`})}),`
|
|
160
|
+
`,(0,c.jsx)(t.h3,{id:`что-такое-ssr-и-hydration`,children:`Что такое SSR и hydration`}),`
|
|
161
|
+
`,(0,c.jsxs)(t.p,{children:[`SSR — создание HTML на сервере, где нет `,(0,c.jsx)(t.code,{children:`window`}),` и неизвестна реальная ширина экрана пользователя.`]}),`
|
|
162
|
+
`,(0,c.jsx)(t.p,{children:`Hydration — подключение React к уже созданному серверному HTML в браузере. Начальный client snapshot должен быть согласован с server snapshot, иначе React может получить различающуюся разметку.`}),`
|
|
163
|
+
`,(0,c.jsxs)(t.p,{children:[`Hooks модуля используют `,(0,c.jsx)(t.code,{children:`useSyncExternalStore`}),` и отдельный server snapshot. Это делает механизм подписки SSR-безопасным, но разработчик всё равно отвечает за UX при последующем изменении значения в браузере.`]}),`
|
|
164
|
+
`,(0,c.jsx)(t.h3,{id:`breakpoint`,children:`Breakpoint`}),`
|
|
165
|
+
`,(0,c.jsx)(t.p,{children:`Breakpoint — именованный диапазон ширины. В модуле их ровно три:`}),`
|
|
166
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type Breakpoint = "mobile" | "tablet" | "laptop";
|
|
167
|
+
`})}),`
|
|
168
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`laptop`}),` охватывает также более широкие desktop viewport. Отдельного `,(0,c.jsx)(t.code,{children:`desktop`}),` в публичном типе нет.`]}),`
|
|
169
|
+
`,(0,c.jsx)(t.h2,{id:`как-выбрать-api`,children:`Как выбрать API`}),`
|
|
170
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Нужно`}),(0,c.jsx)(t.th,{children:`API`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Проверить `,(0,c.jsx)(t.code,{children:`prefers-color-scheme`}),`, `,(0,c.jsx)(t.code,{children:`prefers-reduced-motion`}),`, `,(0,c.jsx)(t.code,{children:`orientation`}),` или собственную ширину`]}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useMediaQuery(query, options?)`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Получить `,(0,c.jsx)(t.code,{children:`mobile`}),` / `,(0,c.jsx)(t.code,{children:`tablet`}),` / `,(0,c.jsx)(t.code,{children:`laptop`}),` по проектным границам`]}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useMatchMedia()`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Описать prop как обычное или responsive-значение`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ResponsiveValue<T>`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Выбрать одно responsive-значение без React`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`resolveResponsiveValue(value, breakpoint)`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Разрешить несколько выбранных props`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`resolveProps(props, breakpoint, keys)`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Проверить runtime-объект на responsive keys`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`isResponsiveMap(value)`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Прочитать вычисленный `,(0,c.jsx)(t.code,{children:`font-size`})]}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getCurrentFontSize(element?)`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Прочитать нормализованный `,(0,c.jsx)(t.code,{children:`line-height`})]}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getCurrentLineHeight(element?)`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Получить проектную высоту control`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getControlHeight()`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Разделить px на базовый размер `,(0,c.jsx)(t.code,{children:`em`})]}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`pxToEm(px, baseEm?)`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Изменить только внешний вид на разных ширинах`}),(0,c.jsxs)(t.td,{children:[`CSS `,(0,c.jsx)(t.code,{children:`@media`}),` или container query`]})]})]})]}),`
|
|
171
|
+
`,(0,c.jsx)(t.h2,{id:`проектные-брейкпоинты`,children:`Проектные брейкпоинты`}),`
|
|
172
|
+
`,(0,c.jsx)(t.h3,{id:`breakpoint-1`,children:(0,c.jsx)(t.code,{children:`Breakpoint`})}),`
|
|
173
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`Breakpoint`}),` — union type:`]}),`
|
|
174
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type Breakpoint = "mobile" | "tablet" | "laptop";
|
|
175
|
+
`})}),`
|
|
176
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import type { Breakpoint } from "@ryuzaki13/react-foundation-lib/media";
|
|
177
|
+
|
|
178
|
+
const valid: Breakpoint = "tablet";
|
|
179
|
+
|
|
180
|
+
// Ошибка TypeScript.
|
|
181
|
+
const invalid: Breakpoint = "desktop";
|
|
182
|
+
`})}),`
|
|
183
|
+
`,(0,c.jsx)(t.h3,{id:`breakpoints_em`,children:(0,c.jsx)(t.code,{children:`BREAKPOINTS_EM`})}),`
|
|
184
|
+
`,(0,c.jsxs)(t.p,{children:[`Константа хранит числовые границы в `,(0,c.jsx)(t.code,{children:`em`}),`:`]}),`
|
|
185
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const BREAKPOINTS_EM = {
|
|
186
|
+
mobileMax: 767.98 / 16,
|
|
187
|
+
tabletMin: 768 / 16,
|
|
188
|
+
tabletMax: 1151.98 / 16,
|
|
189
|
+
laptopMin: 1152 / 16
|
|
190
|
+
} as const;
|
|
191
|
+
`})}),`
|
|
192
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Поле`}),(0,c.jsxs)(t.th,{style:{textAlign:`right`},children:[`Значение в `,(0,c.jsx)(t.code,{children:`em`})]}),(0,c.jsxs)(t.th,{style:{textAlign:`right`},children:[`При базе `,(0,c.jsx)(t.code,{children:`16px`})]}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`mobileMax`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`47.99875`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`767.98px`})}),(0,c.jsx)(t.td,{children:`верхняя граница mobile`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`tabletMin`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`48`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`768px`})}),(0,c.jsx)(t.td,{children:`нижняя граница tablet`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`tabletMax`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`71.99875`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`1151.98px`})}),(0,c.jsx)(t.td,{children:`верхняя граница tablet`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`laptopMin`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`72`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`1152px`})}),(0,c.jsx)(t.td,{children:`нижняя граница laptop`})]})]})]}),`
|
|
193
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`BREAKPOINTS_EM`}),` имеет `,(0,c.jsx)(t.code,{children:`as const`}),`-тип. Не изменяйте его поля в consumer-коде.`]}),`
|
|
194
|
+
`,(0,c.jsx)(t.h3,{id:`mediamatches`,children:(0,c.jsx)(t.code,{children:`MediaMatches`})}),`
|
|
195
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`MediaMatches`}),` хранит boolean для каждого breakpoint:`]}),`
|
|
196
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type MediaMatches = Record<Breakpoint, boolean>;
|
|
197
|
+
`})}),`
|
|
198
|
+
`,(0,c.jsx)(t.p,{children:`Эквивалентная форма:`}),`
|
|
199
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type MediaMatches = {
|
|
200
|
+
mobile: boolean;
|
|
201
|
+
tablet: boolean;
|
|
202
|
+
laptop: boolean;
|
|
203
|
+
};
|
|
204
|
+
`})}),`
|
|
205
|
+
`,(0,c.jsx)(t.h3,{id:`как-границы-зависят-от-root-font-size`,children:`Как границы зависят от root font-size`}),`
|
|
206
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`useMatchMedia`}),` читает `,(0,c.jsx)(t.code,{children:`font-size`}),` у `,(0,c.jsx)(t.code,{children:`document.documentElement`}),` и строит query в пикселях:`]}),`
|
|
207
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`граница в px = BREAKPOINTS_EM.<поле> × текущий root font-size
|
|
208
|
+
`})}),`
|
|
209
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{style:{textAlign:`right`},children:`Root font-size`}),(0,c.jsx)(t.th,{children:`Mobile`}),(0,c.jsx)(t.th,{children:`Tablet`}),(0,c.jsx)(t.th,{children:`Laptop`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`16px`})}),(0,c.jsxs)(t.td,{children:[`до `,(0,c.jsx)(t.code,{children:`767.98px`})]}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`768px`}),` — `,(0,c.jsx)(t.code,{children:`1151.98px`})]}),(0,c.jsxs)(t.td,{children:[`от `,(0,c.jsx)(t.code,{children:`1152px`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`18px`})}),(0,c.jsxs)(t.td,{children:[`до `,(0,c.jsx)(t.code,{children:`863.9775px`})]}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`864px`}),` — `,(0,c.jsx)(t.code,{children:`1295.9775px`})]}),(0,c.jsxs)(t.td,{children:[`от `,(0,c.jsx)(t.code,{children:`1296px`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`20px`})}),(0,c.jsxs)(t.td,{children:[`до `,(0,c.jsx)(t.code,{children:`959.975px`})]}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`960px`}),` — `,(0,c.jsx)(t.code,{children:`1439.975px`})]}),(0,c.jsxs)(t.td,{children:[`от `,(0,c.jsx)(t.code,{children:`1440px`})]})]})]})]}),`
|
|
210
|
+
`,(0,c.jsxs)(t.p,{children:[`Это отличается от жёстко записанного `,(0,c.jsx)(t.code,{children:`(width < 768px)`}),`: пользователь с увеличенным базовым шрифтом дольше остаётся в более компактном layout.`]}),`
|
|
211
|
+
`,(0,c.jsx)(t.h3,{id:`микрозазоры-между-диапазонами`,children:`Микрозазоры между диапазонами`}),`
|
|
212
|
+
`,(0,c.jsxs)(t.p,{children:[`Между `,(0,c.jsx)(t.code,{children:`mobileMax`}),` и `,(0,c.jsx)(t.code,{children:`tabletMin`}),`, а также между `,(0,c.jsx)(t.code,{children:`tabletMax`}),` и `,(0,c.jsx)(t.code,{children:`laptopMin`}),` есть разница `,(0,c.jsx)(t.code,{children:`0.02px`}),` при базе `,(0,c.jsx)(t.code,{children:`16px`}),`. Она предотвращает одновременное совпадение соседних диапазонов на общей границе.`]}),`
|
|
213
|
+
`,(0,c.jsxs)(t.p,{children:[`При дробной ширине внутри такого очень узкого зазора все три `,(0,c.jsx)(t.code,{children:`matches`}),` теоретически могут быть `,(0,c.jsx)(t.code,{children:`false`}),`. В этом случае `,(0,c.jsx)(t.code,{children:`activeBreakpoint`}),` будет `,(0,c.jsx)(t.code,{children:`"laptop"`}),`, потому что это fallback алгоритма.`]}),`
|
|
214
|
+
`,(0,c.jsxs)(t.p,{children:[`Не воспроизводите границы вручную. Используйте `,(0,c.jsx)(t.code,{children:`BREAKPOINTS_EM`}),` или `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),`, чтобы разные компоненты не получили расходящиеся значения.`]}),`
|
|
215
|
+
`,(0,c.jsx)(t.h2,{id:`usemediaquery`,children:(0,c.jsx)(t.code,{children:`useMediaQuery`})}),`
|
|
216
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`useMediaQuery`}),` подписывает React-компонент на один произвольный media query и возвращает boolean.`]}),`
|
|
217
|
+
`,(0,c.jsx)(t.h3,{id:`сигнатура`,children:`Сигнатура`}),`
|
|
218
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`function useMediaQuery(
|
|
219
|
+
query: string,
|
|
220
|
+
options?: {
|
|
221
|
+
readonly ssrFallback?: boolean;
|
|
222
|
+
}
|
|
223
|
+
): boolean;
|
|
224
|
+
`})}),`
|
|
225
|
+
`,(0,c.jsx)(t.p,{children:`Тип options является частью сигнатуры функции, но не экспортируется отдельным именем.`}),`
|
|
226
|
+
`,(0,c.jsx)(t.h3,{id:`параметры`,children:`Параметры`}),`
|
|
227
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Параметр`}),(0,c.jsx)(t.th,{children:`Тип`}),(0,c.jsx)(t.th,{children:`Default`}),(0,c.jsx)(t.th,{children:`Значение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`query`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`string`})}),(0,c.jsx)(t.td,{children:`обязательный`}),(0,c.jsxs)(t.td,{children:[`условие для `,(0,c.jsx)(t.code,{children:`window.matchMedia`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`options.ssrFallback`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`boolean`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`false`})}),(0,c.jsx)(t.td,{children:`значение на сервере и начальной hydration-фазе`})]})]})]}),`
|
|
228
|
+
`,(0,c.jsx)(t.h3,{id:`возвращаемое-значение`,children:`Возвращаемое значение`}),`
|
|
229
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
230
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`true`}),` — query сейчас совпадает;`]}),`
|
|
231
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`false`}),` — query не совпадает или browser API недоступен и fallback равен `,(0,c.jsx)(t.code,{children:`false`}),`.`]}),`
|
|
232
|
+
`]}),`
|
|
233
|
+
`,(0,c.jsx)(t.h3,{id:`минимальный-пример`,children:`Минимальный пример`}),`
|
|
234
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useMediaQuery } from "@ryuzaki13/react-foundation-lib/media";
|
|
235
|
+
|
|
236
|
+
export function ColorSchemeStatus() {
|
|
237
|
+
const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
|
|
238
|
+
|
|
239
|
+
return <p>Системная тема: {prefersDark ? "тёмная" : "светлая"}</p>;
|
|
240
|
+
}
|
|
241
|
+
`})}),`
|
|
242
|
+
`,(0,c.jsxs)(t.p,{children:[`Передавайте только внутреннее условие. Не добавляйте CSS-директиву `,(0,c.jsx)(t.code,{children:`@media`}),`:`]}),`
|
|
243
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`// Правильно.
|
|
244
|
+
useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
245
|
+
|
|
246
|
+
// Неправильно.
|
|
247
|
+
useMediaQuery("@media (prefers-reduced-motion: reduce)");
|
|
248
|
+
`})}),`
|
|
249
|
+
`,(0,c.jsx)(t.h3,{id:`доступность-reduced-motion`,children:`Доступность: reduced motion`}),`
|
|
250
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useMediaQuery } from "@ryuzaki13/react-foundation-lib/media";
|
|
251
|
+
|
|
252
|
+
export function AnimatedProgress({ value }: { value: number }) {
|
|
253
|
+
const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
254
|
+
const label = "Прогресс: " + value + "%";
|
|
255
|
+
const transform = "scaleX(" + value / 100 + ")";
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div
|
|
259
|
+
aria-label={label}
|
|
260
|
+
style={{
|
|
261
|
+
transform,
|
|
262
|
+
transition: reduceMotion ? "none" : "transform 200ms ease"
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
`})}),`
|
|
268
|
+
`,(0,c.jsx)(t.p,{children:`Preference должна улучшать доступность, но сам hook не добавляет ARIA и не изменяет CSS.`}),`
|
|
269
|
+
`,(0,c.jsx)(t.h3,{id:`ssr-fallback`,children:`SSR fallback`}),`
|
|
270
|
+
`,(0,c.jsxs)(t.p,{children:[`На сервере нельзя узнать `,(0,c.jsx)(t.code,{children:`window.matchMedia(...).matches`}),`. Поэтому hook возвращает `,(0,c.jsx)(t.code,{children:`ssrFallback`}),`:`]}),`
|
|
271
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const prefersDark = useMediaQuery("(prefers-color-scheme: dark)", {
|
|
272
|
+
ssrFallback: false
|
|
273
|
+
});
|
|
274
|
+
`})}),`
|
|
275
|
+
`,(0,c.jsx)(t.p,{children:`Во время hydration React сначала использует то же fallback-значение, затем читает реальный query в браузере и при необходимости перерисовывает компонент.`}),`
|
|
276
|
+
`,(0,c.jsx)(t.p,{children:`Выбирайте fallback по смыслу:`}),`
|
|
277
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
278
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`false`}),` подходит для безопасного базового поведения;`]}),`
|
|
279
|
+
`,(0,c.jsx)(t.li,{children:`значение из server cookie подходит, если сервер и клиент используют один источник;`}),`
|
|
280
|
+
`,(0,c.jsxs)(t.li,{children:[`значение из заранее выставленного `,(0,c.jsx)(t.code,{children:`data-*`}),` атрибута можно передать, если оно согласовано с серверной разметкой;`]}),`
|
|
281
|
+
`,(0,c.jsxs)(t.li,{children:[`не пытайтесь читать `,(0,c.jsx)(t.code,{children:`window`}),` прямо на сервере.`]}),`
|
|
282
|
+
`]}),`
|
|
283
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`type Scheme = "light" | "dark";
|
|
284
|
+
|
|
285
|
+
function useSystemDarkScheme(serverScheme: Scheme) {
|
|
286
|
+
return useMediaQuery("(prefers-color-scheme: dark)", {
|
|
287
|
+
ssrFallback: serverScheme === "dark"
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
`})}),`
|
|
291
|
+
`,(0,c.jsx)(t.h3,{id:`lifecycle-подписки`,children:`Lifecycle подписки`}),`
|
|
292
|
+
`,(0,c.jsxs)(t.ol,{children:[`
|
|
293
|
+
`,(0,c.jsxs)(t.li,{children:[`React читает snapshot через `,(0,c.jsx)(t.code,{children:`window.matchMedia(query).matches`}),`.`]}),`
|
|
294
|
+
`,(0,c.jsxs)(t.li,{children:[`Hook подписывается на событие `,(0,c.jsx)(t.code,{children:`change`}),` соответствующего `,(0,c.jsx)(t.code,{children:`MediaQueryList`}),`.`]}),`
|
|
295
|
+
`,(0,c.jsx)(t.li,{children:`При изменении browser вызывает listener.`}),`
|
|
296
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`useSyncExternalStore`}),` повторно читает snapshot.`]}),`
|
|
297
|
+
`,(0,c.jsx)(t.li,{children:`React перерисовывает component, если boolean изменился.`}),`
|
|
298
|
+
`,(0,c.jsxs)(t.li,{children:[`При изменении строки `,(0,c.jsx)(t.code,{children:`query`}),` старая подписка очищается и создаётся новая.`]}),`
|
|
299
|
+
`,(0,c.jsx)(t.li,{children:`При unmount listener удаляется.`}),`
|
|
300
|
+
`]}),`
|
|
301
|
+
`,(0,c.jsxs)(t.p,{children:[`Современные браузеры используют `,(0,c.jsx)(t.code,{children:`addEventListener("change", ...)`}),`. Для legacy Safari предусмотрен fallback на `,(0,c.jsx)(t.code,{children:`addListener`}),` / `,(0,c.jsx)(t.code,{children:`removeListener`}),`.`]}),`
|
|
302
|
+
`,(0,c.jsx)(t.h3,{id:`динамический-query`,children:`Динамический query`}),`
|
|
303
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useMemo } from "react";
|
|
304
|
+
|
|
305
|
+
import { useMediaQuery } from "@ryuzaki13/react-foundation-lib/media";
|
|
306
|
+
|
|
307
|
+
export function WidthStatus({ minWidth }: { minWidth: number }) {
|
|
308
|
+
const query = useMemo(() => "(width >= " + minWidth + "px)", [minWidth]);
|
|
309
|
+
const matches = useMediaQuery(query);
|
|
310
|
+
|
|
311
|
+
return <span>{matches ? "Достаточно места" : "Мало места"}</span>;
|
|
312
|
+
}
|
|
313
|
+
`})}),`
|
|
314
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`useMemo`}),` здесь не обязателен: React dependency сравнивается по строковому значению. Он только делает зависимость query от `,(0,c.jsx)(t.code,{children:`minWidth`}),` очевидной.`]}),`
|
|
315
|
+
`,(0,c.jsxs)(t.h3,{id:`если-matchmedia-отсутствует`,children:[`Если `,(0,c.jsx)(t.code,{children:`matchMedia`}),` отсутствует`]}),`
|
|
316
|
+
`,(0,c.jsxs)(t.p,{children:[`В Node.js или неполном test DOM `,(0,c.jsx)(t.code,{children:`window.matchMedia`}),` может отсутствовать. Тогда:`]}),`
|
|
317
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
318
|
+
`,(0,c.jsxs)(t.li,{children:[`snapshot равен `,(0,c.jsx)(t.code,{children:`ssrFallback`}),`;`]}),`
|
|
319
|
+
`,(0,c.jsx)(t.li,{children:`фактическая подписка не создаётся;`}),`
|
|
320
|
+
`,(0,c.jsx)(t.li,{children:`hook не устанавливает polyfill;`}),`
|
|
321
|
+
`,(0,c.jsx)(t.li,{children:`результат не реагирует на изменение viewport в этой среде.`}),`
|
|
322
|
+
`]}),`
|
|
323
|
+
`,(0,c.jsx)(t.h3,{id:`когда-использовать`,children:`Когда использовать`}),`
|
|
324
|
+
`,(0,c.jsx)(t.p,{children:`Подходящие случаи:`}),`
|
|
325
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
326
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`prefers-color-scheme`}),`;`]}),`
|
|
327
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`prefers-reduced-motion`}),`;`]}),`
|
|
328
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`prefers-contrast`}),`;`]}),`
|
|
329
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`orientation`}),`;`]}),`
|
|
330
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`hover`}),` и `,(0,c.jsx)(t.code,{children:`pointer`}),`;`]}),`
|
|
331
|
+
`,(0,c.jsx)(t.li,{children:`интеграция с API, которому нужен boolean;`}),`
|
|
332
|
+
`,(0,c.jsx)(t.li,{children:`изменение вычислительного поведения, которое нельзя выразить CSS.`}),`
|
|
333
|
+
`]}),`
|
|
334
|
+
`,(0,c.jsxs)(t.p,{children:[`Для проектных `,(0,c.jsx)(t.code,{children:`mobile`}),` / `,(0,c.jsx)(t.code,{children:`tablet`}),` / `,(0,c.jsx)(t.code,{children:`laptop`}),` предпочтительнее `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),`: он использует единые границы и отслеживает изменение базового размера шрифта.`]}),`
|
|
335
|
+
`,(0,c.jsx)(t.h3,{id:`ограничения`,children:`Ограничения`}),`
|
|
336
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
337
|
+
`,(0,c.jsx)(t.li,{children:`строка query передаётся браузеру без валидации;`}),`
|
|
338
|
+
`,(0,c.jsxs)(t.li,{children:[`hook не возвращает сам `,(0,c.jsx)(t.code,{children:`MediaQueryList`}),`;`]}),`
|
|
339
|
+
`,(0,c.jsx)(t.li,{children:`hook не сообщает причину изменения;`}),`
|
|
340
|
+
`,(0,c.jsx)(t.li,{children:`для каждого вызова создаётся отдельная подписка;`}),`
|
|
341
|
+
`,(0,c.jsx)(t.li,{children:`browser определяет поддержку конкретной media feature;`}),`
|
|
342
|
+
`,(0,c.jsx)(t.li,{children:`результат capability query не обязательно описывает текущий способ действия пользователя.`}),`
|
|
343
|
+
`]}),`
|
|
344
|
+
`,(0,c.jsx)(t.h2,{id:`usematchmedia`,children:(0,c.jsx)(t.code,{children:`useMatchMedia`})}),`
|
|
345
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`useMatchMedia`}),` возвращает проектный breakpoint и сырые boolean-совпадения для всех трёх диапазонов.`]}),`
|
|
346
|
+
`,(0,c.jsx)(t.h3,{id:`сигнатура-результата`,children:`Сигнатура результата`}),`
|
|
347
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`function useMatchMedia(): {
|
|
348
|
+
matches: MediaMatches;
|
|
349
|
+
activeBreakpoint: Breakpoint;
|
|
350
|
+
};
|
|
351
|
+
`})}),`
|
|
352
|
+
`,(0,c.jsxs)(t.p,{children:[`Тип объекта snapshot не экспортируется отдельным именем. Используйте `,(0,c.jsx)(t.code,{children:`Breakpoint`}),` и `,(0,c.jsx)(t.code,{children:`MediaMatches`}),` для своих контрактов либо выводите return type:`]}),`
|
|
353
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { useMatchMedia } from "@ryuzaki13/react-foundation-lib/media";
|
|
354
|
+
|
|
355
|
+
type MatchMediaResult = ReturnType<typeof useMatchMedia>;
|
|
356
|
+
`})}),`
|
|
357
|
+
`,(0,c.jsx)(t.h3,{id:`минимальный-пример-1`,children:`Минимальный пример`}),`
|
|
358
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useMatchMedia } from "@ryuzaki13/react-foundation-lib/media";
|
|
359
|
+
|
|
360
|
+
export function BreakpointBadge() {
|
|
361
|
+
const { activeBreakpoint, matches } = useMatchMedia();
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<p>
|
|
365
|
+
Активный диапазон: {activeBreakpoint}. Mobile query: {matches.mobile ? "совпадает" : "не совпадает"}.
|
|
366
|
+
</p>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
`})}),`
|
|
370
|
+
`,(0,c.jsxs)(t.p,{children:[`Для обычного выбора используйте `,(0,c.jsx)(t.code,{children:`activeBreakpoint`}),`. `,(0,c.jsx)(t.code,{children:`matches`}),` нужен для диагностики или когда важны исходные результаты каждого query.`]}),`
|
|
371
|
+
`,(0,c.jsx)(t.h3,{id:`точные-query`,children:`Точные query`}),`
|
|
372
|
+
`,(0,c.jsxs)(t.p,{children:[`При текущем root font-size `,(0,c.jsx)(t.code,{children:`E`}),` hook создаёт:`]}),`
|
|
373
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`mobile: (width <= mobileMax × E px)
|
|
374
|
+
tablet: (width >= tabletMin × E px) and (width <= tabletMax × E px)
|
|
375
|
+
laptop: (width >= laptopMin × E px)
|
|
376
|
+
`})}),`
|
|
377
|
+
`,(0,c.jsxs)(t.p,{children:[`При `,(0,c.jsx)(t.code,{children:`E = 16`}),` итог примерно такой:`]}),`
|
|
378
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`mobile: (width <= 767.98px)
|
|
379
|
+
tablet: (width >= 768px) and (width <= 1151.98px)
|
|
380
|
+
laptop: (width >= 1152px)
|
|
381
|
+
`})}),`
|
|
382
|
+
`,(0,c.jsxs)(t.p,{children:[`Hook использует современный range-синтаксис media features. Если поддерживаемый browser не понимает этот синтаксис, соответствующий `,(0,c.jsx)(t.code,{children:`MediaQueryList.matches`}),` останется `,(0,c.jsx)(t.code,{children:`false`}),`.`]}),`
|
|
383
|
+
`,(0,c.jsxs)(t.h3,{id:`как-выбирается-activebreakpoint`,children:[`Как выбирается `,(0,c.jsx)(t.code,{children:`activeBreakpoint`})]}),`
|
|
384
|
+
`,(0,c.jsx)(t.p,{children:`Приоритет:`}),`
|
|
385
|
+
`,(0,c.jsxs)(t.ol,{children:[`
|
|
386
|
+
`,(0,c.jsxs)(t.li,{children:[`если `,(0,c.jsx)(t.code,{children:`matches.mobile === true`}),`, результат `,(0,c.jsx)(t.code,{children:`"mobile"`}),`;`]}),`
|
|
387
|
+
`,(0,c.jsxs)(t.li,{children:[`иначе, если `,(0,c.jsx)(t.code,{children:`matches.tablet === true`}),`, результат `,(0,c.jsx)(t.code,{children:`"tablet"`}),`;`]}),`
|
|
388
|
+
`,(0,c.jsxs)(t.li,{children:[`иначе результат `,(0,c.jsx)(t.code,{children:`"laptop"`}),`.`]}),`
|
|
389
|
+
`]}),`
|
|
390
|
+
`,(0,c.jsx)(t.p,{children:`Следствия:`}),`
|
|
391
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
392
|
+
`,(0,c.jsx)(t.li,{children:`при нормальном диапазоне активен ровно один breakpoint;`}),`
|
|
393
|
+
`,(0,c.jsxs)(t.li,{children:[`если по какой-либо причине совпали несколько, `,(0,c.jsx)(t.code,{children:`mobile`}),` имеет наивысший приоритет;`]}),`
|
|
394
|
+
`,(0,c.jsxs)(t.li,{children:[`если не совпал ни один, используется `,(0,c.jsx)(t.code,{children:`laptop`}),`;`]}),`
|
|
395
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`activeBreakpoint`}),` никогда не равен `,(0,c.jsx)(t.code,{children:`undefined`}),`.`]}),`
|
|
396
|
+
`]}),`
|
|
397
|
+
`,(0,c.jsx)(t.h3,{id:`ssr-snapshot`,children:`SSR snapshot`}),`
|
|
398
|
+
`,(0,c.jsx)(t.p,{children:`На сервере hook всегда возвращает стабильный default snapshot:`}),`
|
|
399
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`{
|
|
400
|
+
matches: {
|
|
401
|
+
mobile: false,
|
|
402
|
+
tablet: false,
|
|
403
|
+
laptop: false
|
|
404
|
+
},
|
|
405
|
+
activeBreakpoint: "laptop"
|
|
406
|
+
}
|
|
407
|
+
`})}),`
|
|
408
|
+
`,(0,c.jsx)(t.p,{children:`Сервер не пытается угадать ширину по User-Agent. На hydration React сначала использует этот же snapshot, затем подписывается и публикует фактическое browser-состояние.`}),`
|
|
409
|
+
`,(0,c.jsx)(t.p,{children:`Это предотвращает несогласованную первую hydration-проверку, но может дать короткий переход с laptop-разметки на mobile-разметку. Если такой переход заметен:`}),`
|
|
410
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
411
|
+
`,(0,c.jsx)(t.li,{children:`используйте CSS для визуального layout;`}),`
|
|
412
|
+
`,(0,c.jsx)(t.li,{children:`не удаляйте важный контент по breakpoint;`}),`
|
|
413
|
+
`,(0,c.jsx)(t.li,{children:`зарезервируйте размеры;`}),`
|
|
414
|
+
`,(0,c.jsx)(t.li,{children:`используйте устойчивую базовую разметку;`}),`
|
|
415
|
+
`,(0,c.jsxs)(t.li,{children:[`не пытайтесь исправить это прямым чтением `,(0,c.jsx)(t.code,{children:`window.innerWidth`}),` во время render.`]}),`
|
|
416
|
+
`]}),`
|
|
417
|
+
`,(0,c.jsx)(t.h3,{id:`singleton-external-store`,children:`Singleton external store`}),`
|
|
418
|
+
`,(0,c.jsxs)(t.p,{children:[`Все mounted вызовы `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),` используют один module-level store.`]}),`
|
|
419
|
+
`,(0,c.jsx)(t.p,{children:`Для всего загруженного экземпляра entrypoint создаются:`}),`
|
|
420
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
421
|
+
`,(0,c.jsxs)(t.li,{children:[`три `,(0,c.jsx)(t.code,{children:`MediaQueryList`}),`;`]}),`
|
|
422
|
+
`,(0,c.jsx)(t.li,{children:`по одному listener на каждый query;`}),`
|
|
423
|
+
`,(0,c.jsxs)(t.li,{children:[`один `,(0,c.jsx)(t.code,{children:`MutationObserver`}),` для root attributes;`]}),`
|
|
424
|
+
`,(0,c.jsxs)(t.li,{children:[`не более одного `,(0,c.jsx)(t.code,{children:`ResizeObserver`}),`;`]}),`
|
|
425
|
+
`,(0,c.jsxs)(t.li,{children:[`один скрытый probe-элемент размером `,(0,c.jsx)(t.code,{children:`1em`}),`;`]}),`
|
|
426
|
+
`,(0,c.jsx)(t.li,{children:`один общий immutable snapshot;`}),`
|
|
427
|
+
`,(0,c.jsx)(t.li,{children:`набор React listeners.`}),`
|
|
428
|
+
`]}),`
|
|
429
|
+
`,(0,c.jsxs)(t.p,{children:[`Если hook используют 100 компонентов, модуль не создаёт 300 независимых `,(0,c.jsx)(t.code,{children:`MediaQueryList`}),`. Компоненты подписываются на общий snapshot.`]}),`
|
|
430
|
+
`,(0,c.jsx)(t.h3,{id:`инициализация`,children:`Инициализация`}),`
|
|
431
|
+
`,(0,c.jsx)(t.p,{children:`Store лениво инициализируется при первом реальном browser subscriber:`}),`
|
|
432
|
+
`,(0,c.jsxs)(t.ol,{children:[`
|
|
433
|
+
`,(0,c.jsxs)(t.li,{children:[`subscriber добавляется в общий `,(0,c.jsx)(t.code,{children:`Set`}),`;`]}),`
|
|
434
|
+
`,(0,c.jsx)(t.li,{children:`читается root font-size;`}),`
|
|
435
|
+
`,(0,c.jsx)(t.li,{children:`создаются три query;`}),`
|
|
436
|
+
`,(0,c.jsx)(t.li,{children:`подключаются query listeners;`}),`
|
|
437
|
+
`,(0,c.jsx)(t.li,{children:`вычисляется первый client snapshot;`}),`
|
|
438
|
+
`,(0,c.jsxs)(t.li,{children:[`подключаются observers за изменением `,(0,c.jsx)(t.code,{children:`em`}),`.`]}),`
|
|
439
|
+
`]}),`
|
|
440
|
+
`,(0,c.jsx)(t.p,{children:`Один импорт модуля не добавляет DOM-элементы и listeners.`}),`
|
|
441
|
+
`,(0,c.jsxs)(t.h3,{id:`отслеживание-root-em`,children:[`Отслеживание root `,(0,c.jsx)(t.code,{children:`em`})]}),`
|
|
442
|
+
`,(0,c.jsx)(t.p,{children:`Изменение базового шрифта может изменить breakpoint даже при неизменной ширине viewport. Store наблюдает:`}),`
|
|
443
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
444
|
+
`,(0,c.jsxs)(t.li,{children:[`атрибуты `,(0,c.jsx)(t.code,{children:`style`}),`, `,(0,c.jsx)(t.code,{children:`class`}),` и `,(0,c.jsx)(t.code,{children:`data-font-size`}),` у `,(0,c.jsx)(t.code,{children:`document.documentElement`}),`;`]}),`
|
|
445
|
+
`,(0,c.jsxs)(t.li,{children:[`размер скрытого probe-элемента `,(0,c.jsx)(t.code,{children:`width: 1em`}),` через `,(0,c.jsx)(t.code,{children:`ResizeObserver`}),`.`]}),`
|
|
446
|
+
`]}),`
|
|
447
|
+
`,(0,c.jsxs)(t.p,{children:[`Observer events объединяются debounce-интервалом `,(0,c.jsx)(t.code,{children:`80ms`}),`. Query перестраиваются, если root font-size изменился минимум на `,(0,c.jsx)(t.code,{children:`0.25px`}),`. Меньшие колебания игнорируются.`]}),`
|
|
448
|
+
`,(0,c.jsx)(t.p,{children:`Например:`}),`
|
|
449
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`document.documentElement.style.fontSize = "20px";
|
|
450
|
+
`})}),`
|
|
451
|
+
`,(0,c.jsxs)(t.p,{children:[`Mutation observer планирует проверку. Если предыдущее значение было `,(0,c.jsx)(t.code,{children:`16px`}),`, query перестраиваются на границы `,(0,c.jsx)(t.code,{children:`960px`}),` и `,(0,c.jsx)(t.code,{children:`1440px`}),`.`]}),`
|
|
452
|
+
`,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`ResizeObserver`}),` отсутствует, store продолжит работать с `,(0,c.jsx)(t.code,{children:`MutationObserver`}),` и media-query listeners, но не получит дополнительный сигнал через probe.`]}),`
|
|
453
|
+
`,(0,c.jsx)(t.h3,{id:`cleanup`,children:`Cleanup`}),`
|
|
454
|
+
`,(0,c.jsx)(t.p,{children:`Когда последний component отписывается:`}),`
|
|
455
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
456
|
+
`,(0,c.jsx)(t.li,{children:`удаляются три media-query listener;`}),`
|
|
457
|
+
`,(0,c.jsx)(t.li,{children:`отменяется ожидающий debounce timer;`}),`
|
|
458
|
+
`,(0,c.jsxs)(t.li,{children:[`отключаются `,(0,c.jsx)(t.code,{children:`MutationObserver`}),` и `,(0,c.jsx)(t.code,{children:`ResizeObserver`}),`;`]}),`
|
|
459
|
+
`,(0,c.jsx)(t.li,{children:`probe удаляется из DOM;`}),`
|
|
460
|
+
`,(0,c.jsxs)(t.li,{children:[`текущие `,(0,c.jsx)(t.code,{children:`MediaQueryList`}),` освобождаются;`]}),`
|
|
461
|
+
`,(0,c.jsx)(t.li,{children:`store помечается неинициализированным.`}),`
|
|
462
|
+
`]}),`
|
|
463
|
+
`,(0,c.jsx)(t.p,{children:`Последний опубликованный snapshot остаётся в памяти модуля. При следующем subscriber store принудительно перечитает root font-size и query.`}),`
|
|
464
|
+
`,(0,c.jsx)(t.h3,{id:`react-strict-mode`,children:`React Strict Mode`}),`
|
|
465
|
+
`,(0,c.jsx)(t.p,{children:`В development React Strict Mode может выполнить дополнительный subscribe/unsubscribe cycle. Store поддерживает это: ресурсы создаются лениво и очищаются после ухода последнего listener.`}),`
|
|
466
|
+
`,(0,c.jsx)(t.p,{children:`Не пишите код, который рассчитывает на единственную инициализацию component за всё время жизни приложения.`}),`
|
|
467
|
+
`,(0,c.jsxs)(t.h3,{id:`когда-использовать-matches`,children:[`Когда использовать `,(0,c.jsx)(t.code,{children:`matches`})]}),`
|
|
468
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const { activeBreakpoint, matches } = useMatchMedia();
|
|
469
|
+
|
|
470
|
+
// Основной consumer-код.
|
|
471
|
+
const columns = activeBreakpoint === "laptop" ? 3 : 1;
|
|
472
|
+
|
|
473
|
+
// Диагностический код.
|
|
474
|
+
console.table(matches);
|
|
475
|
+
`})}),`
|
|
476
|
+
`,(0,c.jsxs)(t.p,{children:[`Не вычисляйте breakpoint повторно из `,(0,c.jsx)(t.code,{children:`matches`}),` без необходимости. `,(0,c.jsx)(t.code,{children:`activeBreakpoint`}),` уже применяет точный приоритет модуля.`]}),`
|
|
477
|
+
`,(0,c.jsx)(t.h3,{id:`ограничения-1`,children:`Ограничения`}),`
|
|
478
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
479
|
+
`,(0,c.jsx)(t.li,{children:`пользовательские breakpoint values передать нельзя;`}),`
|
|
480
|
+
`,(0,c.jsx)(t.li,{children:`SSR fallback настроить нельзя;`}),`
|
|
481
|
+
`,(0,c.jsx)(t.li,{children:`границы зависят от root font-size;`}),`
|
|
482
|
+
`,(0,c.jsxs)(t.li,{children:[`изменение stylesheet отслеживается через фактическое изменение probe, если доступен `,(0,c.jsx)(t.code,{children:`ResizeObserver`}),`;`]}),`
|
|
483
|
+
`,(0,c.jsxs)(t.li,{children:[`частичная browser-like среда должна иметь `,(0,c.jsx)(t.code,{children:`MutationObserver`}),`;`]}),`
|
|
484
|
+
`,(0,c.jsx)(t.li,{children:`hook отслеживает viewport, а не отдельный container;`}),`
|
|
485
|
+
`,(0,c.jsx)(t.li,{children:`store один на загруженный экземпляр модуля, но две физические копии пакета создадут два store;`}),`
|
|
486
|
+
`,(0,c.jsxs)(t.li,{children:[`очень узкие зазоры диапазонов получают fallback `,(0,c.jsx)(t.code,{children:`laptop`}),`.`]}),`
|
|
487
|
+
`]}),`
|
|
488
|
+
`,(0,c.jsx)(t.h2,{id:`responsive-значения`,children:`Responsive-значения`}),`
|
|
489
|
+
`,(0,c.jsx)(t.p,{children:`Responsive-значение позволяет prop принимать либо одно значение для всех breakpoint, либо map с разными вариантами.`}),`
|
|
490
|
+
`,(0,c.jsx)(t.h3,{id:`responsivemapt`,children:(0,c.jsx)(t.code,{children:`ResponsiveMap<T>`})}),`
|
|
491
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type ResponsiveMap<T> = {
|
|
492
|
+
mobile?: T;
|
|
493
|
+
tablet?: T;
|
|
494
|
+
laptop?: T;
|
|
495
|
+
};
|
|
496
|
+
`})}),`
|
|
497
|
+
`,(0,c.jsx)(t.p,{children:`Все поля optional. Допустимы полная и частичная map:`}),`
|
|
498
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import type { ResponsiveMap } from "@ryuzaki13/react-foundation-lib/media";
|
|
499
|
+
|
|
500
|
+
const full: ResponsiveMap<number> = {
|
|
501
|
+
mobile: 1,
|
|
502
|
+
tablet: 2,
|
|
503
|
+
laptop: 3
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const partial: ResponsiveMap<number> = {
|
|
507
|
+
mobile: 1,
|
|
508
|
+
laptop: 3
|
|
509
|
+
};
|
|
510
|
+
`})}),`
|
|
511
|
+
`,(0,c.jsx)(t.p,{children:`Для пропущенного поля resolver использует documented fallback, а не автоматически наследует строго «слева направо».`}),`
|
|
512
|
+
`,(0,c.jsx)(t.h3,{id:`responsivevaluet`,children:(0,c.jsx)(t.code,{children:`ResponsiveValue<T>`})}),`
|
|
513
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type ResponsiveValue<T> = T | ResponsiveMap<T>;
|
|
514
|
+
`})}),`
|
|
515
|
+
`,(0,c.jsx)(t.p,{children:`Пример component prop:`}),`
|
|
516
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import type { ResponsiveValue } from "@ryuzaki13/react-foundation-lib/media";
|
|
517
|
+
|
|
518
|
+
type PanelProps = {
|
|
519
|
+
gap?: ResponsiveValue<"small" | "medium" | "large">;
|
|
520
|
+
};
|
|
521
|
+
`})}),`
|
|
522
|
+
`,(0,c.jsx)(t.p,{children:`Consumer может передать обычное значение:`}),`
|
|
523
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`<Panel gap="medium" />
|
|
524
|
+
`})}),`
|
|
525
|
+
`,(0,c.jsx)(t.p,{children:`Оно применяется на всех breakpoint. Или map:`}),`
|
|
526
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`<Panel
|
|
527
|
+
gap={{
|
|
528
|
+
mobile: "small",
|
|
529
|
+
tablet: "medium",
|
|
530
|
+
laptop: "large"
|
|
531
|
+
}}
|
|
532
|
+
/>
|
|
533
|
+
`})}),`
|
|
534
|
+
`,(0,c.jsx)(t.h3,{id:`unwrapresponsivet`,children:(0,c.jsx)(t.code,{children:`UnwrapResponsive<T>`})}),`
|
|
535
|
+
`,(0,c.jsxs)(t.p,{children:[`Conditional type снимает обёртку `,(0,c.jsx)(t.code,{children:`ResponsiveValue`}),`:`]}),`
|
|
536
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type UnwrapResponsive<T> = T extends ResponsiveValue<infer U> ? U : T;
|
|
537
|
+
`})}),`
|
|
538
|
+
`,(0,c.jsx)(t.p,{children:`Концептуальные примеры:`}),`
|
|
539
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type A = UnwrapResponsive<ResponsiveValue<number>>;
|
|
540
|
+
// number
|
|
541
|
+
|
|
542
|
+
type B = UnwrapResponsive<string>;
|
|
543
|
+
// string
|
|
544
|
+
`})}),`
|
|
545
|
+
`,(0,c.jsxs)(t.p,{children:[`Это type-level операция. В runtime она ничего не вычисляет. Для реального выбора значения используйте `,(0,c.jsx)(t.code,{children:`resolveResponsiveValue`}),` или `,(0,c.jsx)(t.code,{children:`resolveProps`}),`.`]}),`
|
|
546
|
+
`,(0,c.jsx)(t.h2,{id:`isresponsivemap`,children:(0,c.jsx)(t.code,{children:`isResponsiveMap`})}),`
|
|
547
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`isResponsiveMap`}),` проверяет runtime-значение и одновременно служит TypeScript type guard.`]}),`
|
|
548
|
+
`,(0,c.jsx)(t.h3,{id:`сигнатура-1`,children:`Сигнатура`}),`
|
|
549
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`function isResponsiveMap<T>(value: ResponsiveValue<T>): value is ResponsiveMap<T>;
|
|
550
|
+
`})}),`
|
|
551
|
+
`,(0,c.jsxs)(t.p,{children:[`Функция возвращает `,(0,c.jsx)(t.code,{children:`true`}),`, если:`]}),`
|
|
552
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
553
|
+
`,(0,c.jsx)(t.li,{children:`значение является object;`}),`
|
|
554
|
+
`,(0,c.jsxs)(t.li,{children:[`значение не равно `,(0,c.jsx)(t.code,{children:`null`}),`;`]}),`
|
|
555
|
+
`,(0,c.jsxs)(t.li,{children:[`в нём присутствует хотя бы один собственный или унаследованный ключ `,(0,c.jsx)(t.code,{children:`mobile`}),`, `,(0,c.jsx)(t.code,{children:`tablet`}),`, `,(0,c.jsx)(t.code,{children:`laptop`}),` или `,(0,c.jsx)(t.code,{children:`desktop`}),`.`]}),`
|
|
556
|
+
`]}),`
|
|
557
|
+
`,(0,c.jsxs)(t.p,{children:[`Проверяется наличие ключа через оператор `,(0,c.jsx)(t.code,{children:`in`}),`, а не значение поля:`]}),`
|
|
558
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`isResponsiveMap({ mobile: "compact" }); // true
|
|
559
|
+
isResponsiveMap({ mobile: undefined }); // true
|
|
560
|
+
isResponsiveMap({ value: "compact" }); // false
|
|
561
|
+
isResponsiveMap("compact"); // false
|
|
562
|
+
isResponsiveMap(null); // false
|
|
563
|
+
`})}),`
|
|
564
|
+
`,(0,c.jsx)(t.h3,{id:`важная-коллизия-object-values`,children:`Важная коллизия object values`}),`
|
|
565
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`ResponsiveValue<T>`}),` может принимать обычный object `,(0,c.jsx)(t.code,{children:`T`}),`. Если у обычного business object есть поле `,(0,c.jsx)(t.code,{children:`mobile`}),`, `,(0,c.jsx)(t.code,{children:`tablet`}),`, `,(0,c.jsx)(t.code,{children:`laptop`}),` или `,(0,c.jsx)(t.code,{children:`desktop`}),`, функция ошибочно с точки зрения бизнес-смысла распознает его как responsive map:`]}),`
|
|
566
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const contact = {
|
|
567
|
+
mobile: "+7 900 000-00-00",
|
|
568
|
+
name: "Анна"
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
isResponsiveMap(contact); // true
|
|
572
|
+
`})}),`
|
|
573
|
+
`,(0,c.jsx)(t.p,{children:`Не используйте responsive wrapper для object-типа с такими ключами. Измените форму данных или явно отделите значение:`}),`
|
|
574
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type ContactValue = {
|
|
575
|
+
value: {
|
|
576
|
+
mobile: string;
|
|
577
|
+
name: string;
|
|
578
|
+
};
|
|
579
|
+
};
|
|
580
|
+
`})}),`
|
|
581
|
+
`,(0,c.jsxs)(t.h3,{id:`почему-распознаётся-desktop`,children:[`Почему распознаётся `,(0,c.jsx)(t.code,{children:`desktop`})]}),`
|
|
582
|
+
`,(0,c.jsxs)(t.p,{children:[`Runtime guard считает объект с ключом `,(0,c.jsx)(t.code,{children:`desktop`}),` responsive map, однако:`]}),`
|
|
583
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
584
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`desktop`}),` не входит в `,(0,c.jsx)(t.code,{children:`ResponsiveMap<T>`}),`;`]}),`
|
|
585
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`desktop`}),` не входит в `,(0,c.jsx)(t.code,{children:`Breakpoint`}),`;`]}),`
|
|
586
|
+
`,(0,c.jsxs)(t.li,{children:[`resolver не читает значение `,(0,c.jsx)(t.code,{children:`desktop`}),`.`]}),`
|
|
587
|
+
`]}),`
|
|
588
|
+
`,(0,c.jsx)(t.p,{children:`Поэтому не передавайте:`}),`
|
|
589
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const legacyValue = {
|
|
590
|
+
desktop: "wide"
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
resolveResponsiveValue(legacyValue, "laptop");
|
|
594
|
+
// undefined
|
|
595
|
+
`})}),`
|
|
596
|
+
`,(0,c.jsxs)(t.p,{children:[`Распознавание `,(0,c.jsx)(t.code,{children:`desktop`}),` следует считать защитой от смешения legacy-подобного объекта с обычным value, а не поддержкой четвёртого breakpoint.`]}),`
|
|
597
|
+
`,(0,c.jsx)(t.h2,{id:`resolveresponsivevalue`,children:(0,c.jsx)(t.code,{children:`resolveResponsiveValue`})}),`
|
|
598
|
+
`,(0,c.jsx)(t.p,{children:`Функция выбирает одно значение для переданного breakpoint. React и DOM ей не нужны.`}),`
|
|
599
|
+
`,(0,c.jsx)(t.h3,{id:`сигнатура-2`,children:`Сигнатура`}),`
|
|
600
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`function resolveResponsiveValue<T>(value: ResponsiveValue<T> | undefined, breakpoint: Breakpoint): T | undefined;
|
|
601
|
+
`})}),`
|
|
602
|
+
`,(0,c.jsx)(t.h3,{id:`обычное-значение`,children:`Обычное значение`}),`
|
|
603
|
+
`,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`value`}),` не является responsive map, оно возвращается без изменений:`]}),`
|
|
604
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`resolveResponsiveValue("medium", "mobile"); // "medium"
|
|
605
|
+
resolveResponsiveValue(42, "tablet"); // 42
|
|
606
|
+
resolveResponsiveValue(null, "laptop"); // null, если T допускает null
|
|
607
|
+
resolveResponsiveValue(undefined, "mobile"); // undefined
|
|
608
|
+
`})}),`
|
|
609
|
+
`,(0,c.jsx)(t.h3,{id:`точный-fallback-порядок`,children:`Точный fallback-порядок`}),`
|
|
610
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Текущий breakpoint`}),(0,c.jsx)(t.th,{children:`Порядок поиска`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`mobile`})}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`mobile`}),` → `,(0,c.jsx)(t.code,{children:`tablet`}),` → `,(0,c.jsx)(t.code,{children:`laptop`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`tablet`})}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`tablet`}),` → `,(0,c.jsx)(t.code,{children:`mobile`}),` → `,(0,c.jsx)(t.code,{children:`laptop`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`laptop`})}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`laptop`}),` → `,(0,c.jsx)(t.code,{children:`tablet`}),` → `,(0,c.jsx)(t.code,{children:`mobile`})]})]})]})]}),`
|
|
611
|
+
`,(0,c.jsx)(t.p,{children:`Примеры:`}),`
|
|
612
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`resolveResponsiveValue(
|
|
613
|
+
{
|
|
614
|
+
tablet: "medium",
|
|
615
|
+
laptop: "large"
|
|
616
|
+
},
|
|
617
|
+
"mobile"
|
|
618
|
+
);
|
|
619
|
+
// "medium"
|
|
620
|
+
|
|
621
|
+
resolveResponsiveValue(
|
|
622
|
+
{
|
|
623
|
+
mobile: "small",
|
|
624
|
+
laptop: "large"
|
|
625
|
+
},
|
|
626
|
+
"tablet"
|
|
627
|
+
);
|
|
628
|
+
// "small"
|
|
629
|
+
|
|
630
|
+
resolveResponsiveValue(
|
|
631
|
+
{
|
|
632
|
+
mobile: "small",
|
|
633
|
+
tablet: "medium"
|
|
634
|
+
},
|
|
635
|
+
"laptop"
|
|
636
|
+
);
|
|
637
|
+
// "medium"
|
|
638
|
+
`})}),`
|
|
639
|
+
`,(0,c.jsxs)(t.p,{children:[`Для `,(0,c.jsx)(t.code,{children:`tablet`}),` сначала берётся точное значение, потом mobile, только затем laptop. Это важно: алгоритм не просто выбирает первое поле в object и не всегда движется к большему breakpoint.`]}),`
|
|
640
|
+
`,(0,c.jsx)(t.h3,{id:`nullish-fallback`,children:`Nullish fallback`}),`
|
|
641
|
+
`,(0,c.jsxs)(t.p,{children:[`Resolver использует оператор `,(0,c.jsx)(t.code,{children:`??`}),`. Отсутствующими считаются `,(0,c.jsx)(t.code,{children:`undefined`}),` и `,(0,c.jsx)(t.code,{children:`null`}),`. Значения `,(0,c.jsx)(t.code,{children:`false`}),`, `,(0,c.jsx)(t.code,{children:`0`}),` и пустая строка остаются полноценными:`]}),`
|
|
642
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`resolveResponsiveValue(
|
|
643
|
+
{
|
|
644
|
+
mobile: false,
|
|
645
|
+
tablet: true
|
|
646
|
+
},
|
|
647
|
+
"mobile"
|
|
648
|
+
);
|
|
649
|
+
// false
|
|
650
|
+
|
|
651
|
+
resolveResponsiveValue(
|
|
652
|
+
{
|
|
653
|
+
mobile: 0,
|
|
654
|
+
tablet: 10
|
|
655
|
+
},
|
|
656
|
+
"mobile"
|
|
657
|
+
);
|
|
658
|
+
// 0
|
|
659
|
+
|
|
660
|
+
resolveResponsiveValue(
|
|
661
|
+
{
|
|
662
|
+
mobile: "",
|
|
663
|
+
tablet: "fallback"
|
|
664
|
+
},
|
|
665
|
+
"mobile"
|
|
666
|
+
);
|
|
667
|
+
// ""
|
|
668
|
+
`})}),`
|
|
669
|
+
`,(0,c.jsxs)(t.p,{children:[`Но `,(0,c.jsx)(t.code,{children:`null`}),` внутри map запускает fallback:`]}),`
|
|
670
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`resolveResponsiveValue(
|
|
671
|
+
{
|
|
672
|
+
mobile: null,
|
|
673
|
+
tablet: "fallback"
|
|
674
|
+
},
|
|
675
|
+
"mobile"
|
|
676
|
+
);
|
|
677
|
+
// "fallback"
|
|
678
|
+
`})}),`
|
|
679
|
+
`,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`null`}),` должен быть отдельным осмысленным responsive-значением, текущий contract для этого не подходит.`]}),`
|
|
680
|
+
`,(0,c.jsx)(t.h3,{id:`пустая-map`,children:`Пустая map`}),`
|
|
681
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`resolveResponsiveValue({}, "tablet"); // undefined
|
|
682
|
+
`})}),`
|
|
683
|
+
`,(0,c.jsxs)(t.p,{children:[`TypeScript разрешает пустой `,(0,c.jsx)(t.code,{children:`ResponsiveMap<T>`}),`, потому что все поля optional. Проверяйте обязательность на уровне props или validation host-приложения.`]}),`
|
|
684
|
+
`,(0,c.jsx)(t.h3,{id:`функция-не-мутирует-данные`,children:`Функция не мутирует данные`}),`
|
|
685
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`resolveResponsiveValue`}),` только читает переданное значение. Она не дописывает отсутствующие поля и не изменяет map.`]}),`
|
|
686
|
+
`,(0,c.jsx)(t.h2,{id:`resolveprops`,children:(0,c.jsx)(t.code,{children:`resolveProps`})}),`
|
|
687
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`resolveProps`}),` создаёт поверхностную копию object props и разрешает только явно перечисленные keys.`]}),`
|
|
688
|
+
`,(0,c.jsx)(t.h3,{id:`сигнатура-3`,children:`Сигнатура`}),`
|
|
689
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`function resolveProps<T extends object>(props: T, breakpoint: Breakpoint, responsiveKeys: readonly (keyof T)[]): ResolvedProps<T>;
|
|
690
|
+
`})}),`
|
|
691
|
+
`,(0,c.jsx)(t.h3,{id:`resolvedpropst`,children:(0,c.jsx)(t.code,{children:`ResolvedProps<T>`})}),`
|
|
692
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type ResolvedProps<T extends object> = {
|
|
693
|
+
[K in keyof T]: UnwrapResponsive<T[K]>;
|
|
694
|
+
};
|
|
695
|
+
`})}),`
|
|
696
|
+
`,(0,c.jsx)(t.p,{children:`Тип сохраняет имена полей и снимает responsive wrapper с их типов.`}),`
|
|
697
|
+
`,(0,c.jsx)(t.h3,{id:`базовый-пример`,children:`Базовый пример`}),`
|
|
698
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { resolveProps } from "@ryuzaki13/react-foundation-lib/media";
|
|
699
|
+
import type { ResponsiveValue } from "@ryuzaki13/react-foundation-lib/media";
|
|
700
|
+
|
|
701
|
+
type LayoutProps = {
|
|
702
|
+
direction?: ResponsiveValue<"row" | "column">;
|
|
703
|
+
gap?: ResponsiveValue<number>;
|
|
704
|
+
title: string;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const props: LayoutProps = {
|
|
708
|
+
direction: {
|
|
709
|
+
mobile: "column",
|
|
710
|
+
laptop: "row"
|
|
711
|
+
},
|
|
712
|
+
gap: {
|
|
713
|
+
mobile: 8,
|
|
714
|
+
tablet: 12,
|
|
715
|
+
laptop: 16
|
|
716
|
+
},
|
|
717
|
+
title: "Профиль"
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const resolved = resolveProps(props, "tablet", ["direction", "gap"]);
|
|
721
|
+
|
|
722
|
+
console.log(resolved);
|
|
723
|
+
// {
|
|
724
|
+
// direction: "column",
|
|
725
|
+
// gap: 12,
|
|
726
|
+
// title: "Профиль"
|
|
727
|
+
// }
|
|
728
|
+
`})}),`
|
|
729
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`title`}),` не был перечислен, поэтому скопирован как есть.`]}),`
|
|
730
|
+
`,(0,c.jsx)(t.h3,{id:`типобезопасный-список-keys`,children:`Типобезопасный список keys`}),`
|
|
731
|
+
`,(0,c.jsx)(t.p,{children:`Объявляйте стабильный список рядом с component:`}),`
|
|
732
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const RESPONSIVE_KEYS = ["direction", "gap"] as const satisfies readonly (keyof LayoutProps)[];
|
|
733
|
+
`})}),`
|
|
734
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`satisfies`}),` проверит опечатку:`]}),`
|
|
735
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const BAD_KEYS = ["direciton"] as const satisfies readonly (keyof LayoutProps)[];
|
|
736
|
+
// Ошибка TypeScript: такого key нет.
|
|
737
|
+
`})}),`
|
|
738
|
+
`,(0,c.jsx)(t.h3,{id:`разрешаются-только-перечисленные-поля`,children:`Разрешаются только перечисленные поля`}),`
|
|
739
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`resolveProps(
|
|
740
|
+
{
|
|
741
|
+
gap: {
|
|
742
|
+
mobile: 8,
|
|
743
|
+
laptop: 16
|
|
744
|
+
},
|
|
745
|
+
padding: {
|
|
746
|
+
mobile: 4,
|
|
747
|
+
laptop: 12
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
"laptop",
|
|
751
|
+
["gap"]
|
|
752
|
+
);
|
|
753
|
+
// {
|
|
754
|
+
// gap: 16,
|
|
755
|
+
// padding: { mobile: 4, laptop: 12 }
|
|
756
|
+
// }
|
|
757
|
+
`})}),`
|
|
758
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`padding`}),` остаётся map. Тип результата предполагает развёрнутые responsive fields, поэтому consumer обязан правильно перечислить все поля, которые действительно имеют `,(0,c.jsx)(t.code,{children:`ResponsiveValue`}),`. TypeScript не может проверить semantic полноту списка.`]}),`
|
|
759
|
+
`,(0,c.jsx)(t.h3,{id:`поверхностная-копия`,children:`Поверхностная копия`}),`
|
|
760
|
+
`,(0,c.jsx)(t.p,{children:`Исходный object верхнего уровня не изменяется:`}),`
|
|
761
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const original = {
|
|
762
|
+
size: {
|
|
763
|
+
mobile: "small",
|
|
764
|
+
laptop: "large"
|
|
765
|
+
},
|
|
766
|
+
meta: {
|
|
767
|
+
id: 1
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const resolved = resolveProps(original, "laptop", ["size"]);
|
|
772
|
+
|
|
773
|
+
console.log(resolved === original); // false
|
|
774
|
+
console.log(resolved.meta === original.meta); // true
|
|
775
|
+
`})}),`
|
|
776
|
+
`,(0,c.jsx)(t.p,{children:`Вложенные обычные objects сохраняют reference. Это не deep clone.`}),`
|
|
777
|
+
`,(0,c.jsx)(t.h3,{id:`отсутствующий-key`,children:`Отсутствующий key`}),`
|
|
778
|
+
`,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`responsiveKeys`}),` содержит optional key, которого нет в `,(0,c.jsx)(t.code,{children:`props`}),`, результат получит это поле со значением `,(0,c.jsx)(t.code,{children:`undefined`}),`:`]}),`
|
|
779
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const result = resolveProps(
|
|
780
|
+
{
|
|
781
|
+
title: "Карточка"
|
|
782
|
+
} as {
|
|
783
|
+
title: string;
|
|
784
|
+
gap?: ResponsiveValue<number>;
|
|
785
|
+
},
|
|
786
|
+
"mobile",
|
|
787
|
+
["gap"]
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
console.log("gap" in result); // true
|
|
791
|
+
console.log(result.gap); // undefined
|
|
792
|
+
`})}),`
|
|
793
|
+
`,(0,c.jsxs)(t.p,{children:[`Это может иметь значение при `,(0,c.jsx)(t.code,{children:`Object.keys`}),`, spread или сериализации custom replacer.`]}),`
|
|
794
|
+
`,(0,c.jsx)(t.h3,{id:`нет-рекурсивного-обхода`,children:`Нет рекурсивного обхода`}),`
|
|
795
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const props = {
|
|
796
|
+
layout: {
|
|
797
|
+
gap: {
|
|
798
|
+
mobile: 8,
|
|
799
|
+
laptop: 16
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
resolveProps(props, "laptop", ["layout"]);
|
|
805
|
+
`})}),`
|
|
806
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`layout`}),` — обычный object без breakpoint keys, поэтому возвращается целиком. Вложенный `,(0,c.jsx)(t.code,{children:`layout.gap`}),` не разрешается. Вызовите resolver на нужном уровне самостоятельно.`]}),`
|
|
807
|
+
`,(0,c.jsx)(t.h2,{id:`полный-react-пример`,children:`Полный React-пример`}),`
|
|
808
|
+
`,(0,c.jsx)(t.p,{children:`Ниже component принимает обычные или responsive props, получает текущий breakpoint и разрешает их перед созданием styles.`}),`
|
|
809
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`import { useMemo } from "react";
|
|
810
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
811
|
+
|
|
812
|
+
import { resolveProps, useMatchMedia } from "@ryuzaki13/react-foundation-lib/media";
|
|
813
|
+
import type { ResponsiveValue } from "@ryuzaki13/react-foundation-lib/media";
|
|
814
|
+
|
|
815
|
+
type AdaptiveCardProps = {
|
|
816
|
+
children: ReactNode;
|
|
817
|
+
direction?: ResponsiveValue<"row" | "column">;
|
|
818
|
+
gap?: ResponsiveValue<number>;
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
const RESPONSIVE_KEYS = ["direction", "gap"] as const satisfies readonly (keyof AdaptiveCardProps)[];
|
|
822
|
+
|
|
823
|
+
export function AdaptiveCard({ children, direction = "column", gap = 12 }: AdaptiveCardProps) {
|
|
824
|
+
const { activeBreakpoint } = useMatchMedia();
|
|
825
|
+
|
|
826
|
+
const resolved = useMemo(
|
|
827
|
+
() =>
|
|
828
|
+
resolveProps(
|
|
829
|
+
{
|
|
830
|
+
children,
|
|
831
|
+
direction,
|
|
832
|
+
gap
|
|
833
|
+
},
|
|
834
|
+
activeBreakpoint,
|
|
835
|
+
RESPONSIVE_KEYS
|
|
836
|
+
),
|
|
837
|
+
[activeBreakpoint, children, direction, gap]
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
const style: CSSProperties = {
|
|
841
|
+
display: "flex",
|
|
842
|
+
flexDirection: resolved.direction,
|
|
843
|
+
gap: resolved.gap
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
return <section style={style}>{resolved.children}</section>;
|
|
847
|
+
}
|
|
848
|
+
`})}),`
|
|
849
|
+
`,(0,c.jsx)(t.p,{children:`Использование:`}),`
|
|
850
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`<AdaptiveCard
|
|
851
|
+
direction={{
|
|
852
|
+
mobile: "column",
|
|
853
|
+
laptop: "row"
|
|
854
|
+
}}
|
|
855
|
+
gap={{
|
|
856
|
+
mobile: 8,
|
|
857
|
+
tablet: 12,
|
|
858
|
+
laptop: 16
|
|
859
|
+
}}>
|
|
860
|
+
<div>Основная часть</div>
|
|
861
|
+
<aside>Дополнительная часть</aside>
|
|
862
|
+
</AdaptiveCard>
|
|
863
|
+
`})}),`
|
|
864
|
+
`,(0,c.jsxs)(t.p,{children:[`На mobile результат `,(0,c.jsx)(t.code,{children:`column / 8`}),`. На tablet `,(0,c.jsx)(t.code,{children:`direction`}),` fallback-ится к mobile и получится `,(0,c.jsx)(t.code,{children:`column / 12`}),`. На laptop результат `,(0,c.jsx)(t.code,{children:`row / 16`}),`.`]}),`
|
|
865
|
+
`,(0,c.jsxs)(t.h3,{id:`когда-usememo-действительно-полезен`,children:[`Когда `,(0,c.jsx)(t.code,{children:`useMemo`}),` действительно полезен`]}),`
|
|
866
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`resolveProps`}),` создаёт новый object при каждом вызове. Если результат передаётся в memoized child, используется как effect dependency или участвует в дорогом вычислении, `,(0,c.jsx)(t.code,{children:`useMemo`}),` сохраняет reference до изменения dependencies.`]}),`
|
|
867
|
+
`,(0,c.jsx)(t.p,{children:`Если object сразу превращается в пару primitive и component простой, memoization может быть лишней. Не добавляйте её механически.`}),`
|
|
868
|
+
`,(0,c.jsx)(t.h3,{id:`почему-styles-всё-равно-лучше-держать-в-css`,children:`Почему styles всё равно лучше держать в CSS`}),`
|
|
869
|
+
`,(0,c.jsx)(t.p,{children:`Пример показывает runtime API, но responsive layout часто проще выразить так:`}),`
|
|
870
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-css`,children:`.card {
|
|
871
|
+
display: flex;
|
|
872
|
+
flex-direction: column;
|
|
873
|
+
gap: 0.5rem;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
@media (width >= 48em) {
|
|
877
|
+
.card {
|
|
878
|
+
gap: 0.75rem;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
@media (width >= 72em) {
|
|
883
|
+
.card {
|
|
884
|
+
flex-direction: row;
|
|
885
|
+
gap: 1rem;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
`})}),`
|
|
889
|
+
`,(0,c.jsx)(t.p,{children:`CSS меняет layout без React render и начинает действовать до hydration. Runtime responsive props оправданы, когда это контракт общего UI primitive или значение нужно JavaScript-коду.`}),`
|
|
890
|
+
`,(0,c.jsxs)(t.p,{children:[`Этот CSS-фрагмент показывает сам подход, но не обещает математически идентичные границы: семантику `,(0,c.jsx)(t.code,{children:`em`}),` внутри CSS media query определяет browser, а `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),` явно умножает constants на текущий computed `,(0,c.jsx)(t.code,{children:`font-size`}),` корневого элемента. Если CSS и JavaScript обязаны совпадать до долей пикселя, используйте единый build-time token contract и отдельно проверьте его в поддерживаемых browsers.`]}),`
|
|
891
|
+
`,(0,c.jsx)(t.h2,{id:`dom-метрики`,children:`DOM-метрики`}),`
|
|
892
|
+
`,(0,c.jsx)(t.p,{children:`Модуль экспортирует три функции, которые читают computed styles. Они не являются hooks и могут вызываться в обычном TypeScript-коде, но реальное DOM-значение доступно только в браузере.`}),`
|
|
893
|
+
`,(0,c.jsx)(t.h2,{id:`getcurrentfontsize`,children:(0,c.jsx)(t.code,{children:`getCurrentFontSize`})}),`
|
|
894
|
+
`,(0,c.jsx)(t.h3,{id:`сигнатура-4`,children:`Сигнатура`}),`
|
|
895
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`function getCurrentFontSize(element?: HTMLElement | null): number;
|
|
896
|
+
`})}),`
|
|
897
|
+
`,(0,c.jsx)(t.h3,{id:`поведение`,children:`Поведение`}),`
|
|
898
|
+
`,(0,c.jsxs)(t.ol,{children:[`
|
|
899
|
+
`,(0,c.jsxs)(t.li,{children:[`Если `,(0,c.jsx)(t.code,{children:`window`}),` или `,(0,c.jsx)(t.code,{children:`document`}),` отсутствует, возвращается `,(0,c.jsx)(t.code,{children:`16`}),`.`]}),`
|
|
900
|
+
`,(0,c.jsxs)(t.li,{children:[`Если `,(0,c.jsx)(t.code,{children:`element`}),` передан, читается его computed `,(0,c.jsx)(t.code,{children:`fontSize`}),`.`]}),`
|
|
901
|
+
`,(0,c.jsxs)(t.li,{children:[`Если `,(0,c.jsx)(t.code,{children:`element`}),` отсутствует или равен `,(0,c.jsx)(t.code,{children:`null`}),`, используется `,(0,c.jsx)(t.code,{children:`document.documentElement`}),`.`]}),`
|
|
902
|
+
`,(0,c.jsxs)(t.li,{children:[`Строка разбирается через `,(0,c.jsx)(t.code,{children:`Number.parseFloat`}),`.`]}),`
|
|
903
|
+
`,(0,c.jsxs)(t.li,{children:[`Если результат не finite или меньше либо равен нулю, возвращается `,(0,c.jsx)(t.code,{children:`16`}),`.`]}),`
|
|
904
|
+
`,(0,c.jsx)(t.li,{children:`Иначе возвращается число в пикселях.`}),`
|
|
905
|
+
`]}),`
|
|
906
|
+
`,(0,c.jsx)(t.p,{children:`Пример:`}),`
|
|
907
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { getCurrentFontSize } from "@ryuzaki13/react-foundation-lib/media";
|
|
908
|
+
|
|
909
|
+
const rootSize = getCurrentFontSize();
|
|
910
|
+
|
|
911
|
+
const element = document.querySelector<HTMLElement>(".card");
|
|
912
|
+
const cardSize = getCurrentFontSize(element);
|
|
913
|
+
`})}),`
|
|
914
|
+
`,(0,c.jsxs)(t.p,{children:[`Если browser вернул `,(0,c.jsx)(t.code,{children:`"20px"`}),`, функция вернёт `,(0,c.jsx)(t.code,{children:`20`}),`. Суффикс `,(0,c.jsx)(t.code,{children:`px`}),` в результате отсутствует.`]}),`
|
|
915
|
+
`,(0,c.jsx)(t.h3,{id:`fallback`,children:`Fallback`}),`
|
|
916
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Среда или computed value`}),(0,c.jsx)(t.th,{style:{textAlign:`right`},children:`Результат`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`SSR / Node.js`}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`16`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`"20px"`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`20`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`"16.5px"`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`16.5`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`"normal"`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`16`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`""`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`16`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`"0px"`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`16`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`отрицательное значение`}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`16`})})]})]})]}),`
|
|
917
|
+
`,(0,c.jsx)(t.p,{children:`Функция не проверяет, подключён ли element к DOM. Результат для detached element определяет browser/test environment.`}),`
|
|
918
|
+
`,(0,c.jsx)(t.h2,{id:`getcurrentlineheight`,children:(0,c.jsx)(t.code,{children:`getCurrentLineHeight`})}),`
|
|
919
|
+
`,(0,c.jsx)(t.h3,{id:`сигнатура-5`,children:`Сигнатура`}),`
|
|
920
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`function getCurrentLineHeight(element?: HTMLElement | null): number;
|
|
921
|
+
`})}),`
|
|
922
|
+
`,(0,c.jsx)(t.h3,{id:`точное-вычисление`,children:`Точное вычисление`}),`
|
|
923
|
+
`,(0,c.jsxs)(t.ol,{children:[`
|
|
924
|
+
`,(0,c.jsxs)(t.li,{children:[`Без `,(0,c.jsx)(t.code,{children:`window`}),` или `,(0,c.jsx)(t.code,{children:`document`}),` возвращается `,(0,c.jsx)(t.code,{children:`1.2`}),`.`]}),`
|
|
925
|
+
`,(0,c.jsxs)(t.li,{children:[`Target выбирается так же, как в `,(0,c.jsx)(t.code,{children:`getCurrentFontSize`}),`.`]}),`
|
|
926
|
+
`,(0,c.jsxs)(t.li,{children:[`Читается computed `,(0,c.jsx)(t.code,{children:`lineHeight`}),`.`]}),`
|
|
927
|
+
`,(0,c.jsxs)(t.li,{children:[`Значение разбирается через `,(0,c.jsx)(t.code,{children:`Number.parseFloat`}),`.`]}),`
|
|
928
|
+
`,(0,c.jsxs)(t.li,{children:[`Невалидное или неположительное значение даёт fallback `,(0,c.jsx)(t.code,{children:`1.2`}),`.`]}),`
|
|
929
|
+
`,(0,c.jsxs)(t.li,{children:[`Валидное значение делится на константу `,(0,c.jsx)(t.code,{children:`16`}),`.`]}),`
|
|
930
|
+
`]}),`
|
|
931
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`result = parseFloat(computed line-height) / 16
|
|
932
|
+
`})}),`
|
|
933
|
+
`,(0,c.jsx)(t.p,{children:`Примеры:`}),`
|
|
934
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// computed line-height: "24px"
|
|
935
|
+
getCurrentLineHeight(element); // 1.5
|
|
936
|
+
|
|
937
|
+
// computed line-height: "32px"
|
|
938
|
+
getCurrentLineHeight(element); // 2
|
|
939
|
+
|
|
940
|
+
// computed line-height: "normal"
|
|
941
|
+
getCurrentLineHeight(element); // 1.2
|
|
942
|
+
`})}),`
|
|
943
|
+
`,(0,c.jsx)(t.h3,{id:`важное-ограничение`,children:`Важное ограничение`}),`
|
|
944
|
+
`,(0,c.jsxs)(t.p,{children:[`Функция не делит `,(0,c.jsx)(t.code,{children:`line-height`}),` на фактический `,(0,c.jsx)(t.code,{children:`font-size`}),` переданного element. Делитель всегда равен `,(0,c.jsx)(t.code,{children:`16`}),`.`]}),`
|
|
945
|
+
`,(0,c.jsxs)(t.p,{children:[`Если element имеет `,(0,c.jsx)(t.code,{children:`font-size: 20px`}),` и `,(0,c.jsx)(t.code,{children:`line-height: 24px`}),`, результат всё равно `,(0,c.jsx)(t.code,{children:`24 / 16 = 1.5`}),`, а не `,(0,c.jsx)(t.code,{children:`24 / 20 = 1.2`}),`.`]}),`
|
|
946
|
+
`,(0,c.jsxs)(t.p,{children:[`Поэтому воспринимайте результат как проектную нормализацию относительно базовых `,(0,c.jsx)(t.code,{children:`16px`}),`, а не как универсальный CSS unitless line-height конкретного элемента.`]}),`
|
|
947
|
+
`,(0,c.jsxs)(t.p,{children:[`Если computed style неожиданно возвращает unitless строку `,(0,c.jsx)(t.code,{children:`"1.5"`}),`, функция вычислит `,(0,c.jsx)(t.code,{children:`1.5 / 16 = 0.09375`}),`. В обычном браузере computed `,(0,c.jsx)(t.code,{children:`lineHeight`}),` чаще представлен в пикселях, но mocks должны возвращать контракт, близкий к browser.`]}),`
|
|
948
|
+
`,(0,c.jsx)(t.h2,{id:`getcontrolheight`,children:(0,c.jsx)(t.code,{children:`getControlHeight`})}),`
|
|
949
|
+
`,(0,c.jsx)(t.h3,{id:`сигнатура-6`,children:`Сигнатура`}),`
|
|
950
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`function getControlHeight(): number;
|
|
951
|
+
`})}),`
|
|
952
|
+
`,(0,c.jsx)(t.p,{children:`Функция не принимает element и использует root DOM-метрики:`}),`
|
|
953
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`getControlHeight()
|
|
954
|
+
= getCurrentFontSize()
|
|
955
|
+
+ getCurrentLineHeight() × 10
|
|
956
|
+
`})}),`
|
|
957
|
+
`,(0,c.jsx)(t.p,{children:`Примеры:`}),`
|
|
958
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Root computed styles`}),(0,c.jsx)(t.th,{children:`Расчёт`}),(0,c.jsx)(t.th,{style:{textAlign:`right`},children:`Результат`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`server fallback`}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`16 + 1.2 × 10`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`28`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`font-size: 16px; line-height: 19.2px`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`16 + 1.2 × 10`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`28`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`font-size: 20px; line-height: 24px`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`20 + 1.5 × 10`})}),(0,c.jsx)(t.td,{style:{textAlign:`right`},children:(0,c.jsx)(t.code,{children:`35`})})]})]})]}),`
|
|
959
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`getControlHeight`}),` только возвращает number. Он не устанавливает CSS custom property `,(0,c.jsx)(t.code,{children:`--control-height`}),` и не меняет DOM.`]}),`
|
|
960
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { getControlHeight } from "@ryuzaki13/react-foundation-lib/media";
|
|
961
|
+
|
|
962
|
+
const controlHeight = getControlHeight();
|
|
963
|
+
|
|
964
|
+
document.documentElement.style.setProperty("--control-height", controlHeight + "px");
|
|
965
|
+
`})}),`
|
|
966
|
+
`,(0,c.jsx)(t.p,{children:`Вызов во время React render прочитает computed styles синхронно. Если значение нужно после изменения DOM, читайте его в подходящем effect или внешнем layout-measurement слое.`}),`
|
|
967
|
+
`,(0,c.jsx)(t.h2,{id:`pxtoem`,children:(0,c.jsx)(t.code,{children:`pxToEm`})}),`
|
|
968
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`pxToEm`}),` делит количество пикселей на базовый размер.`]}),`
|
|
969
|
+
`,(0,c.jsx)(t.h3,{id:`сигнатура-7`,children:`Сигнатура`}),`
|
|
970
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`function pxToEm(px: number, baseEm?: number): string;
|
|
971
|
+
`})}),`
|
|
972
|
+
`,(0,c.jsx)(t.h3,{id:`формула`,children:`Формула`}),`
|
|
973
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`result = String(px / (baseEm ?? getCurrentFontSize()))
|
|
974
|
+
`})}),`
|
|
975
|
+
`,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`baseEm`}),` передан, DOM не читается:`]}),`
|
|
976
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { pxToEm } from "@ryuzaki13/react-foundation-lib/media";
|
|
977
|
+
|
|
978
|
+
pxToEm(24, 12); // "2"
|
|
979
|
+
pxToEm(10, 20); // "0.5"
|
|
980
|
+
`})}),`
|
|
981
|
+
`,(0,c.jsxs)(t.p,{children:[`Если `,(0,c.jsx)(t.code,{children:`baseEm`}),` не передан, используется текущий root font-size:`]}),`
|
|
982
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// При root font-size: 20px.
|
|
983
|
+
pxToEm(10); // "0.5"
|
|
984
|
+
`})}),`
|
|
985
|
+
`,(0,c.jsxs)(t.h3,{id:`результат-не-содержит-em`,children:[`Результат не содержит `,(0,c.jsx)(t.code,{children:`em`})]}),`
|
|
986
|
+
`,(0,c.jsx)(t.p,{children:`Несмотря на имя функции, возвращается только числовая строка:`}),`
|
|
987
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`pxToEm(32, 16); // "2", а не "2em"
|
|
988
|
+
`})}),`
|
|
989
|
+
`,(0,c.jsx)(t.p,{children:`Чтобы получить готовое CSS-значение, добавьте unit:`}),`
|
|
990
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`const padding = pxToEm(32, 16) + "em";
|
|
991
|
+
// "2em"
|
|
992
|
+
`})}),`
|
|
993
|
+
`,(0,c.jsx)(t.p,{children:`Не добавляйте unit, если consumer ожидает unitless число в строковой форме.`}),`
|
|
994
|
+
`,(0,c.jsx)(t.h3,{id:`нет-округления`,children:`Нет округления`}),`
|
|
995
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`pxToEm(10, 3);
|
|
996
|
+
// "3.3333333333333335"
|
|
997
|
+
`})}),`
|
|
998
|
+
`,(0,c.jsx)(t.p,{children:`Если UI contract требует фиксированное количество знаков, округляйте результат отдельно и осознанно.`}),`
|
|
999
|
+
`,(0,c.jsx)(t.h3,{id:`валидация-отсутствует`,children:`Валидация отсутствует`}),`
|
|
1000
|
+
`,(0,c.jsx)(t.p,{children:`Функция не проверяет вход:`}),`
|
|
1001
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`pxToEm(10, 0); // "Infinity"
|
|
1002
|
+
pxToEm(-10, 20); // "-0.5"
|
|
1003
|
+
pxToEm(Number.NaN, 16); // "NaN"
|
|
1004
|
+
`})}),`
|
|
1005
|
+
`,(0,c.jsxs)(t.p,{children:[`Передавайте finite `,(0,c.jsx)(t.code,{children:`px`}),` и положительный finite `,(0,c.jsx)(t.code,{children:`baseEm`}),`. `,(0,c.jsx)(t.code,{children:`baseEm = 0`}),` не означает «использовать default», потому что оператор `,(0,c.jsx)(t.code,{children:`??`}),` считает ноль переданным значением.`]}),`
|
|
1006
|
+
`,(0,c.jsx)(t.h3,{id:`ssr`,children:`SSR`}),`
|
|
1007
|
+
`,(0,c.jsxs)(t.p,{children:[`Без `,(0,c.jsx)(t.code,{children:`window`}),` и `,(0,c.jsx)(t.code,{children:`document`}),` `,(0,c.jsx)(t.code,{children:`getCurrentFontSize`}),` возвращает `,(0,c.jsx)(t.code,{children:`16`}),`:`]}),`
|
|
1008
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// В Node.js.
|
|
1009
|
+
pxToEm(32); // "2"
|
|
1010
|
+
`})}),`
|
|
1011
|
+
`,(0,c.jsxs)(t.p,{children:[`Это делает вызов предсказуемым, но server result может отличаться от browser result, если root font-size в браузере не равен `,(0,c.jsx)(t.code,{children:`16px`}),`.`]}),`
|
|
1012
|
+
`,(0,c.jsx)(t.h2,{id:`css-или-javascript`,children:`CSS или JavaScript`}),`
|
|
1013
|
+
`,(0,c.jsx)(t.p,{children:`Выбор инструмента влияет на производительность, SSR и доступность.`}),`
|
|
1014
|
+
`,(0,c.jsx)(t.h3,{id:`используйте-css-когда`,children:`Используйте CSS, когда`}),`
|
|
1015
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
1016
|
+
`,(0,c.jsx)(t.li,{children:`меняется только display, spacing, grid/flex layout, typography или decoration;`}),`
|
|
1017
|
+
`,(0,c.jsx)(t.li,{children:`обе версии контента семантически одинаковы;`}),`
|
|
1018
|
+
`,(0,c.jsx)(t.li,{children:`адаптивность должна работать до загрузки JavaScript;`}),`
|
|
1019
|
+
`,(0,c.jsx)(t.li,{children:`условие относится к размеру конкретного container;`}),`
|
|
1020
|
+
`,(0,c.jsx)(t.li,{children:`не нужен boolean в TypeScript.`}),`
|
|
1021
|
+
`]}),`
|
|
1022
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-css`,children:`.navigation {
|
|
1023
|
+
display: none;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
@media (width >= 48em) {
|
|
1027
|
+
.navigation {
|
|
1028
|
+
display: flex;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
`})}),`
|
|
1032
|
+
`,(0,c.jsxs)(t.h3,{id:`используйте-usemediaquery-когда`,children:[`Используйте `,(0,c.jsx)(t.code,{children:`useMediaQuery`}),`, когда`]}),`
|
|
1033
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
1034
|
+
`,(0,c.jsx)(t.li,{children:`нужен произвольный user preference;`}),`
|
|
1035
|
+
`,(0,c.jsx)(t.li,{children:`результат передаётся в JavaScript API;`}),`
|
|
1036
|
+
`,(0,c.jsx)(t.li,{children:`меняется алгоритм, а не только style;`}),`
|
|
1037
|
+
`,(0,c.jsx)(t.li,{children:`нужен один boolean, не связанный с project breakpoint;`}),`
|
|
1038
|
+
`,(0,c.jsx)(t.li,{children:`SSR fallback можно определить явно.`}),`
|
|
1039
|
+
`]}),`
|
|
1040
|
+
`,(0,c.jsxs)(t.h3,{id:`используйте-usematchmedia-когда`,children:[`Используйте `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),`, когда`]}),`
|
|
1041
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
1042
|
+
`,(0,c.jsxs)(t.li,{children:[`нужен единый `,(0,c.jsx)(t.code,{children:`Breakpoint`}),`;`]}),`
|
|
1043
|
+
`,(0,c.jsxs)(t.li,{children:[`component принимает `,(0,c.jsx)(t.code,{children:`ResponsiveValue`}),`;`]}),`
|
|
1044
|
+
`,(0,c.jsx)(t.li,{children:`несколько foundation UI primitives должны использовать одинаковые границы;`}),`
|
|
1045
|
+
`,(0,c.jsx)(t.li,{children:`требуется динамическая привязка границ к root font-size.`}),`
|
|
1046
|
+
`]}),`
|
|
1047
|
+
`,(0,c.jsx)(t.h3,{id:`не-дублируйте-один-layout-без-необходимости`,children:`Не дублируйте один layout без необходимости`}),`
|
|
1048
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`// Рискованный вариант: две большие ветви DOM.
|
|
1049
|
+
return isMobile ? <MobileCheckout /> : <DesktopCheckout />;
|
|
1050
|
+
`})}),`
|
|
1051
|
+
`,(0,c.jsx)(t.p,{children:`Такой код может дать заметную замену после hydration, создаёт две реализации одного сценария, усложняет focus/state и требует тестировать обе ветви.`}),`
|
|
1052
|
+
`,(0,c.jsx)(t.p,{children:`Предпочтительнее одна семантическая разметка и CSS layout. Разделяйте component по breakpoint только когда поведение действительно различается.`}),`
|
|
1053
|
+
`,(0,c.jsx)(t.h2,{id:`ssr-и-hydration`,children:`SSR и hydration`}),`
|
|
1054
|
+
`,(0,c.jsx)(t.h3,{id:`сводная-таблица`,children:`Сводная таблица`}),`
|
|
1055
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`API`}),(0,c.jsx)(t.th,{children:`Server result`}),(0,c.jsx)(t.th,{children:`Первый hydration snapshot`}),(0,c.jsx)(t.th,{children:`Browser update`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useMediaQuery(query)`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`false`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`false`})}),(0,c.jsxs)(t.td,{children:[`реальный `,(0,c.jsx)(t.code,{children:`matches`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useMediaQuery(query, { ssrFallback: true })`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`true`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`true`})}),(0,c.jsxs)(t.td,{children:[`реальный `,(0,c.jsx)(t.code,{children:`matches`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useMatchMedia()`})}),(0,c.jsxs)(t.td,{children:[`все `,(0,c.jsx)(t.code,{children:`matches: false`}),`, active `,(0,c.jsx)(t.code,{children:`laptop`})]}),(0,c.jsx)(t.td,{children:`тот же default snapshot`}),(0,c.jsx)(t.td,{children:`реальный breakpoint`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getCurrentFontSize()`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`16`})}),(0,c.jsx)(t.td,{children:`не является hook`}),(0,c.jsx)(t.td,{children:`browser computed value при вызове`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getCurrentLineHeight()`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`1.2`})}),(0,c.jsx)(t.td,{children:`не является hook`}),(0,c.jsx)(t.td,{children:`browser computed value при вызове`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getControlHeight()`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`28`})}),(0,c.jsx)(t.td,{children:`не является hook`}),(0,c.jsx)(t.td,{children:`browser calculation при вызове`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`pxToEm(px)`})}),(0,c.jsxs)(t.td,{children:[`база `,(0,c.jsx)(t.code,{children:`16`})]}),(0,c.jsx)(t.td,{children:`не является hook`}),(0,c.jsx)(t.td,{children:`текущая root-база при вызове`})]})]})]}),`
|
|
1056
|
+
`,(0,c.jsx)(t.h3,{id:`hook-безопасен-не-значит-ui-неизменен`,children:`Hook безопасен не значит UI неизменен`}),`
|
|
1057
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`useSyncExternalStore`}),` согласует server snapshot с начальной hydration-фазой. После подключения browser snapshot может отличаться, и React выполнит обычный update:`]}),`
|
|
1058
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-text`,children:`server: laptop fallback
|
|
1059
|
+
hydration start: laptop fallback
|
|
1060
|
+
browser subscription: mobile
|
|
1061
|
+
React update: mobile UI
|
|
1062
|
+
`})}),`
|
|
1063
|
+
`,(0,c.jsx)(t.p,{children:`Hydration error не обязателен, но пользователь может увидеть layout shift. CSS обычно лучше для геометрии.`}),`
|
|
1064
|
+
`,(0,c.jsx)(t.h3,{id:`не-используйте-browser-api-в-module-scope`,children:`Не используйте browser API в module scope`}),`
|
|
1065
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`// Неправильно для SSR: выполняется при импорте module.
|
|
1066
|
+
const IS_MOBILE = window.matchMedia("(width < 768px)").matches;
|
|
1067
|
+
|
|
1068
|
+
// Правильно внутри React component.
|
|
1069
|
+
function Component() {
|
|
1070
|
+
const isMobile = useMediaQuery("(width < 768px)");
|
|
1071
|
+
return <span>{isMobile ? "mobile" : "wide"}</span>;
|
|
1072
|
+
}
|
|
1073
|
+
`})}),`
|
|
1074
|
+
`,(0,c.jsxs)(t.p,{children:[`Server fallback должен быть детерминированным. Не используйте `,(0,c.jsx)(t.code,{children:`Math.random()`}),`, текущее время или другой нестабильный источник для `,(0,c.jsx)(t.code,{children:`ssrFallback`}),`.`]}),`
|
|
1075
|
+
`,(0,c.jsx)(t.h2,{id:`доступность`,children:`Доступность`}),`
|
|
1076
|
+
`,(0,c.jsx)(t.h3,{id:`user-preferences`,children:`User preferences`}),`
|
|
1077
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
1078
|
+
|
|
1079
|
+
const highContrast = useMediaQuery("(prefers-contrast: more)");
|
|
1080
|
+
`})}),`
|
|
1081
|
+
`,(0,c.jsx)(t.p,{children:`Не заставляйте пользователя включать preference внутри приложения, если browser уже сообщает её. При наличии явной настройки приложения заранее определите приоритет: explicit user setting обычно важнее system preference.`}),`
|
|
1082
|
+
`,(0,c.jsx)(t.h3,{id:`не-удаляйте-смысловой-контент-по-ширине`,children:`Не удаляйте смысловой контент по ширине`}),`
|
|
1083
|
+
`,(0,c.jsxs)(t.p,{children:[`Mobile-пользователь должен получать ту же основную информацию и действия. Можно менять порядок и плотность, но нельзя делать ключевой action доступным только на `,(0,c.jsx)(t.code,{children:`laptop`}),`.`]}),`
|
|
1084
|
+
`,(0,c.jsx)(t.h3,{id:`focus-при-смене-breakpoint`,children:`Focus при смене breakpoint`}),`
|
|
1085
|
+
`,(0,c.jsx)(t.p,{children:`Если React ветка с focused element unmount-ится при изменении breakpoint, focus может потеряться. Для responsive overlay/navigation:`}),`
|
|
1086
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
1087
|
+
`,(0,c.jsx)(t.li,{children:`сохраняйте логическую action;`}),`
|
|
1088
|
+
`,(0,c.jsx)(t.li,{children:`восстанавливайте focus осознанно;`}),`
|
|
1089
|
+
`,(0,c.jsx)(t.li,{children:`используйте DOM focus helpers из соседнего модуля;`}),`
|
|
1090
|
+
`,(0,c.jsx)(t.li,{children:`тестируйте keyboard navigation до и после смены диапазона.`}),`
|
|
1091
|
+
`]}),`
|
|
1092
|
+
`,(0,c.jsx)(t.h3,{id:`query-не-доказывает-способ-ввода`,children:`Query не доказывает способ ввода`}),`
|
|
1093
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`(pointer: coarse)`}),` или `,(0,c.jsx)(t.code,{children:`(hover: none)`}),` описывает возможности среды. Hybrid device может иметь touch, mouse и keyboard одновременно. Не отключайте keyboard behavior и доступные click targets на основании одного query.`]}),`
|
|
1094
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`prefers-reduced-motion`}),` не означает «удалить всю обратную связь». Уберите декоративное перемещение и длительные переходы, но сохраните понятное состояние завершения действия.`]}),`
|
|
1095
|
+
`,(0,c.jsx)(t.h2,{id:`производительность-и-lifecycle`,children:`Производительность и lifecycle`}),`
|
|
1096
|
+
`,(0,c.jsx)(t.h3,{id:`usemediaquery-1`,children:(0,c.jsx)(t.code,{children:`useMediaQuery`})}),`
|
|
1097
|
+
`,(0,c.jsx)(t.p,{children:`Каждый вызов создаёт собственную subscription. Для нескольких разных preferences это нормально. Не создавайте сотни одинаковых calls в элементах большого списка.`}),`
|
|
1098
|
+
`,(0,c.jsx)(t.p,{children:`Поднимите общее значение выше:`}),`
|
|
1099
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-tsx`,children:`function Results({ items }: { items: Item[] }) {
|
|
1100
|
+
const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
1101
|
+
|
|
1102
|
+
return items.map((item) => <ResultRow key={item.id} item={item} reduceMotion={reduceMotion} />);
|
|
1103
|
+
}
|
|
1104
|
+
`})}),`
|
|
1105
|
+
`,(0,c.jsx)(t.h3,{id:`usematchmedia-1`,children:(0,c.jsx)(t.code,{children:`useMatchMedia`})}),`
|
|
1106
|
+
`,(0,c.jsx)(t.p,{children:`Hook уже разделяет singleton store между всеми consumers. Дополнительный React context только ради уменьшения числа browser listeners не нужен.`}),`
|
|
1107
|
+
`,(0,c.jsx)(t.p,{children:`Каждый subscriber всё равно получает React update при изменении общего snapshot. Не запускайте в render тяжёлый transformation.`}),`
|
|
1108
|
+
`,(0,c.jsx)(t.h3,{id:`responsive-resolvers`,children:`Responsive resolvers`}),`
|
|
1109
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
1110
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`resolveResponsiveValue`}),` выполняет несколько сравнений и очень дешёв;`]}),`
|
|
1111
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`resolveProps`}),` выполняет shallow spread и проходит только по `,(0,c.jsx)(t.code,{children:`responsiveKeys`}),`;`]}),`
|
|
1112
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`resolveProps`}),` всегда создаёт новый top-level object;`]}),`
|
|
1113
|
+
`,(0,c.jsx)(t.li,{children:`вложенные references не клонируются;`}),`
|
|
1114
|
+
`,(0,c.jsx)(t.li,{children:`функция не кеширует результат.`}),`
|
|
1115
|
+
`]}),`
|
|
1116
|
+
`,(0,c.jsx)(t.h3,{id:`dom-reads`,children:`DOM reads`}),`
|
|
1117
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`getCurrentFontSize`}),` и `,(0,c.jsx)(t.code,{children:`getCurrentLineHeight`}),` вызывают `,(0,c.jsx)(t.code,{children:`window.getComputedStyle`}),`. Это синхронный DOM read. В hot loop или для тысяч elements он может быть дорогим.`]}),`
|
|
1118
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`// Не делайте так для большой коллекции.
|
|
1119
|
+
for (const element of thousandsOfElements) {
|
|
1120
|
+
getCurrentFontSize(element);
|
|
1121
|
+
}
|
|
1122
|
+
`})}),`
|
|
1123
|
+
`,(0,c.jsx)(t.p,{children:`Если метрика одна и та же, прочитайте её один раз и переиспользуйте. Не смешивайте множество DOM writes и reads в цикле.`}),`
|
|
1124
|
+
`,(0,c.jsx)(t.h3,{id:`изменение-root-font-size`,children:`Изменение root font-size`}),`
|
|
1125
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`useMatchMedia`}),` debounce-ит observer events на `,(0,c.jsx)(t.code,{children:`80ms`}),` и игнорирует изменение меньше `,(0,c.jsx)(t.code,{children:`0.25px`}),`. Поэтому update breakpoint после смены theme/class не обязан произойти синхронно в той же строке JavaScript.`]}),`
|
|
1126
|
+
`,(0,c.jsxs)(t.p,{children:[`После `,(0,c.jsx)(t.code,{children:`document.documentElement.classList.add("large-font")`}),` дождитесь React update.`]}),`
|
|
1127
|
+
`,(0,c.jsx)(t.h2,{id:`тестирование`,children:`Тестирование`}),`
|
|
1128
|
+
`,(0,c.jsx)(t.h3,{id:`pure-helpers`,children:`Pure helpers`}),`
|
|
1129
|
+
`,(0,c.jsxs)(t.p,{children:[(0,c.jsx)(t.code,{children:`resolveResponsiveValue`}),` и `,(0,c.jsx)(t.code,{children:`resolveProps`}),` тестируются без DOM:`]}),`
|
|
1130
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { describe, expect, it } from "vitest";
|
|
1131
|
+
|
|
1132
|
+
import { resolveProps, resolveResponsiveValue } from "@ryuzaki13/react-foundation-lib/media";
|
|
1133
|
+
|
|
1134
|
+
describe("responsive layout", () => {
|
|
1135
|
+
it("использует mobile fallback на tablet", () => {
|
|
1136
|
+
expect(
|
|
1137
|
+
resolveResponsiveValue(
|
|
1138
|
+
{
|
|
1139
|
+
mobile: "column",
|
|
1140
|
+
laptop: "row"
|
|
1141
|
+
},
|
|
1142
|
+
"tablet"
|
|
1143
|
+
)
|
|
1144
|
+
).toBe("column");
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
it("разрешает только перечисленные props", () => {
|
|
1148
|
+
expect(
|
|
1149
|
+
resolveProps(
|
|
1150
|
+
{
|
|
1151
|
+
gap: {
|
|
1152
|
+
mobile: 8,
|
|
1153
|
+
laptop: 16
|
|
1154
|
+
},
|
|
1155
|
+
title: "Карточка"
|
|
1156
|
+
},
|
|
1157
|
+
"laptop",
|
|
1158
|
+
["gap"]
|
|
1159
|
+
)
|
|
1160
|
+
).toEqual({
|
|
1161
|
+
gap: 16,
|
|
1162
|
+
title: "Карточка"
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
});
|
|
1166
|
+
`})}),`
|
|
1167
|
+
`,(0,c.jsxs)(t.p,{children:[`Обязательно проверяйте exact match, fallback для каждого breakpoint, `,(0,c.jsx)(t.code,{children:`false`}),`, `,(0,c.jsx)(t.code,{children:`0`}),`, `,(0,c.jsx)(t.code,{children:`""`}),`, `,(0,c.jsx)(t.code,{children:`null`}),`, пустую map, обычный object и object с конфликтующим key.`]}),`
|
|
1168
|
+
`,(0,c.jsx)(t.h3,{id:`dom-metrics`,children:`DOM metrics`}),`
|
|
1169
|
+
`,(0,c.jsxs)(t.p,{children:[`В jsdom явно mock-айте `,(0,c.jsx)(t.code,{children:`getComputedStyle`}),`:`]}),`
|
|
1170
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`import { afterEach, expect, it, vi } from "vitest";
|
|
1171
|
+
|
|
1172
|
+
import { getCurrentFontSize, getCurrentLineHeight } from "@ryuzaki13/react-foundation-lib/media";
|
|
1173
|
+
|
|
1174
|
+
afterEach(() => {
|
|
1175
|
+
vi.restoreAllMocks();
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
it("читает font metrics", () => {
|
|
1179
|
+
const element = document.createElement("div");
|
|
1180
|
+
|
|
1181
|
+
vi.spyOn(window, "getComputedStyle").mockReturnValue({
|
|
1182
|
+
fontSize: "20px",
|
|
1183
|
+
lineHeight: "24px"
|
|
1184
|
+
} as CSSStyleDeclaration);
|
|
1185
|
+
|
|
1186
|
+
expect(getCurrentFontSize(element)).toBe(20);
|
|
1187
|
+
expect(getCurrentLineHeight(element)).toBe(1.5);
|
|
1188
|
+
});
|
|
1189
|
+
`})}),`
|
|
1190
|
+
`,(0,c.jsx)(t.p,{children:`Не рассчитывайте, что jsdom сам выполнит полноценную CSS layout engine.`}),`
|
|
1191
|
+
`,(0,c.jsxs)(t.h3,{id:`mock-matchmedia`,children:[`Mock `,(0,c.jsx)(t.code,{children:`matchMedia`})]}),`
|
|
1192
|
+
`,(0,c.jsx)(t.p,{children:`Mock должен разделять состояние между всеми вызовами одного query. Реализация hook может отдельно запросить snapshot и отдельно создать subscription.`}),`
|
|
1193
|
+
`,(0,c.jsx)(t.p,{children:`Минимальный contract:`}),`
|
|
1194
|
+
`,(0,c.jsx)(t.pre,{children:(0,c.jsx)(t.code,{className:`language-ts`,children:`type ChangeListener = (event: MediaQueryListEvent) => void;
|
|
1195
|
+
|
|
1196
|
+
const listeners = new Map<string, Set<ChangeListener>>();
|
|
1197
|
+
const matchesByQuery = new Map<string, boolean>();
|
|
1198
|
+
|
|
1199
|
+
Object.defineProperty(window, "matchMedia", {
|
|
1200
|
+
configurable: true,
|
|
1201
|
+
value: vi.fn((query: string): MediaQueryList => {
|
|
1202
|
+
const queryListeners = listeners.get(query) ?? new Set<ChangeListener>();
|
|
1203
|
+
|
|
1204
|
+
listeners.set(query, queryListeners);
|
|
1205
|
+
|
|
1206
|
+
return {
|
|
1207
|
+
media: query,
|
|
1208
|
+
get matches() {
|
|
1209
|
+
return matchesByQuery.get(query) ?? false;
|
|
1210
|
+
},
|
|
1211
|
+
onchange: null,
|
|
1212
|
+
addEventListener: (_type: "change", listener: ChangeListener) => {
|
|
1213
|
+
queryListeners.add(listener);
|
|
1214
|
+
},
|
|
1215
|
+
removeEventListener: (_type: "change", listener: ChangeListener) => {
|
|
1216
|
+
queryListeners.delete(listener);
|
|
1217
|
+
},
|
|
1218
|
+
addListener: (listener: ChangeListener) => {
|
|
1219
|
+
queryListeners.add(listener);
|
|
1220
|
+
},
|
|
1221
|
+
removeListener: (listener: ChangeListener) => {
|
|
1222
|
+
queryListeners.delete(listener);
|
|
1223
|
+
},
|
|
1224
|
+
dispatchEvent: () => true
|
|
1225
|
+
} as MediaQueryList;
|
|
1226
|
+
})
|
|
1227
|
+
});
|
|
1228
|
+
`})}),`
|
|
1229
|
+
`,(0,c.jsxs)(t.p,{children:[`Для переключения состояния обновите `,(0,c.jsx)(t.code,{children:`matchesByQuery`}),` и вызовите listeners внутри `,(0,c.jsx)(t.code,{children:`act`}),` вашей React test utility.`]}),`
|
|
1230
|
+
`,(0,c.jsxs)(t.h3,{id:`особенности-теста-usematchmedia`,children:[`Особенности теста `,(0,c.jsx)(t.code,{children:`useMatchMedia`})]}),`
|
|
1231
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
1232
|
+
`,(0,c.jsx)(t.li,{children:`hook создаёт три query с динамическими pixel values;`}),`
|
|
1233
|
+
`,(0,c.jsxs)(t.li,{children:[`перед тестом задайте ожидаемый root `,(0,c.jsx)(t.code,{children:`font-size`}),`;`]}),`
|
|
1234
|
+
`,(0,c.jsxs)(t.li,{children:[`jsdom обычно требует mock `,(0,c.jsx)(t.code,{children:`ResizeObserver`}),` либо его отсутствие;`]}),`
|
|
1235
|
+
`,(0,c.jsxs)(t.li,{children:[(0,c.jsx)(t.code,{children:`MutationObserver`}),` должен существовать в browser-like среде;`]}),`
|
|
1236
|
+
`,(0,c.jsx)(t.li,{children:`после теста unmount component, чтобы singleton store очистился;`}),`
|
|
1237
|
+
`,(0,c.jsxs)(t.li,{children:[`при проверке font-size учитывайте debounce `,(0,c.jsx)(t.code,{children:`80ms`}),`;`]}),`
|
|
1238
|
+
`,(0,c.jsxs)(t.li,{children:[`fake timers нужно продвинуть минимум на `,(0,c.jsx)(t.code,{children:`80ms`}),` внутри React `,(0,c.jsx)(t.code,{children:`act`}),`;`]}),`
|
|
1239
|
+
`,(0,c.jsx)(t.li,{children:`module-level snapshot может сохраняться между тестами.`}),`
|
|
1240
|
+
`]}),`
|
|
1241
|
+
`,(0,c.jsx)(t.p,{children:`В consumer component проверяйте server fallback, переходы между breakpoint, cleanup после unmount, изменение root font-size и focus при смене layout.`}),`
|
|
1242
|
+
`,(0,c.jsx)(t.h2,{id:`частые-ошибки`,children:`Частые ошибки`}),`
|
|
1243
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Ошибка`}),(0,c.jsx)(t.th,{children:`Фактическое поведение / исправление`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Импорт из корня пакета`}),(0,c.jsxs)(t.td,{children:[`Используйте точный `,(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/media`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Hook внутри `,(0,c.jsx)(t.code,{children:`if (window)`})]}),(0,c.jsx)(t.td,{children:`SSR-защита встроена; условный hook нарушает Rules of Hooks`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`desktop`}),` как breakpoint`]}),(0,c.jsxs)(t.td,{children:[`Публичное имя широкого диапазона — `,(0,c.jsx)(t.code,{children:`laptop`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Ожидание другого tablet fallback`}),(0,c.jsxs)(t.td,{children:[`Порядок: `,(0,c.jsx)(t.code,{children:`tablet → mobile → laptop`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Ожидание truthy fallback`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`false`}),`, `,(0,c.jsx)(t.code,{children:`0`}),` и `,(0,c.jsx)(t.code,{children:`""`}),` сохраняются; используется `,(0,c.jsx)(t.code,{children:`??`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Business object имеет key `,(0,c.jsx)(t.code,{children:`mobile`})]}),(0,c.jsx)(t.td,{children:`Он будет распознан как responsive map`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Ожидание deep resolve`}),(0,c.jsxs)(t.td,{children:[(0,c.jsx)(t.code,{children:`resolveProps`}),` обрабатывает только перечисленные top-level keys`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Неполный `,(0,c.jsx)(t.code,{children:`responsiveKeys`})]}),(0,c.jsx)(t.td,{children:`Runtime не разрешит пропущенное responsive-поле`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsxs)(t.td,{children:[`Ожидание `,(0,c.jsx)(t.code,{children:`"2em"`}),` от `,(0,c.jsx)(t.code,{children:`pxToEm`})]}),(0,c.jsxs)(t.td,{children:[`Функция вернёт `,(0,c.jsx)(t.code,{children:`"2"`}),`; unit добавляет consumer`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`pxToEm(10, 0)`})}),(0,c.jsxs)(t.td,{children:[`Получится `,(0,c.jsx)(t.code,{children:`"Infinity"`}),`; валидации нет`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Line-height делится на font-size element`}),(0,c.jsxs)(t.td,{children:[`Он всегда делится на `,(0,c.jsx)(t.code,{children:`16`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Style-only layout сделан через hook`}),(0,c.jsx)(t.td,{children:`Предпочтите CSS и избегайте post-hydration render`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`Проверка сразу после смены root class`}),(0,c.jsxs)(t.td,{children:[`Observer path имеет debounce `,(0,c.jsx)(t.code,{children:`80ms`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:`В jsdom нет mock`}),(0,c.jsxs)(t.td,{children:[`Предоставьте `,(0,c.jsx)(t.code,{children:`matchMedia`}),` и при необходимости observers`]})]})]})]}),`
|
|
1244
|
+
`,(0,c.jsx)(t.h2,{id:`api-справочник`,children:`API-справочник`}),`
|
|
1245
|
+
`,(0,c.jsx)(t.h3,{id:`runtime-exports`,children:`Runtime exports`}),`
|
|
1246
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Сигнатура / результат`}),(0,c.jsx)(t.th,{children:`Назначение`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`BREAKPOINTS_EM`})}),(0,c.jsx)(t.td,{children:`readonly object`}),(0,c.jsxs)(t.td,{children:[`четыре границы mobile/tablet/laptop в `,(0,c.jsx)(t.code,{children:`em`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getCurrentFontSize`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`(element?: HTMLElement | null) => number`})}),(0,c.jsxs)(t.td,{children:[`computed font-size или fallback `,(0,c.jsx)(t.code,{children:`16`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getCurrentLineHeight`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`(element?: HTMLElement | null) => number`})}),(0,c.jsxs)(t.td,{children:[`parsed line-height, делённый на `,(0,c.jsx)(t.code,{children:`16`}),`, либо `,(0,c.jsx)(t.code,{children:`1.2`})]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`getControlHeight`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`() => number`})}),(0,c.jsx)(t.td,{children:`root font-size + нормализованный line-height × 10`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`pxToEm`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`(px: number, baseEm?: number) => string`})}),(0,c.jsxs)(t.td,{children:[`числовая строка `,(0,c.jsx)(t.code,{children:`px / base`}),` без unit`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`isResponsiveMap`})}),(0,c.jsx)(t.td,{children:`type guard`}),(0,c.jsx)(t.td,{children:`проверка breakpoint keys в object`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`resolveResponsiveValue`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`(value, breakpoint) => T | undefined`})}),(0,c.jsx)(t.td,{children:`выбор exact/fallback значения`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`resolveProps`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`(props, breakpoint, keys) => ResolvedProps<T>`})}),(0,c.jsx)(t.td,{children:`shallow resolve выбранных top-level props`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useMediaQuery`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`(query, options?) => boolean`})}),(0,c.jsx)(t.td,{children:`React subscription на произвольный query`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`useMatchMedia`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`() => { matches, activeBreakpoint }`})}),(0,c.jsx)(t.td,{children:`singleton subscription на project breakpoints`})]})]})]}),`
|
|
1247
|
+
`,(0,c.jsx)(t.h3,{id:`type-exports`,children:`Type exports`}),`
|
|
1248
|
+
`,(0,c.jsxs)(t.table,{children:[(0,c.jsx)(t.thead,{children:(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.th,{children:`Export`}),(0,c.jsx)(t.th,{children:`Contract`})]})}),(0,c.jsxs)(t.tbody,{children:[(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`Breakpoint`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`"mobile" | "tablet" | "laptop"`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`MediaMatches`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`Record<Breakpoint, boolean>`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ResponsiveMap<T>`})}),(0,c.jsxs)(t.td,{children:[`optional `,(0,c.jsx)(t.code,{children:`mobile`}),` / `,(0,c.jsx)(t.code,{children:`tablet`}),` / `,(0,c.jsx)(t.code,{children:`laptop`}),` values`]})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ResponsiveValue<T>`})}),(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`T | ResponsiveMap<T>`})})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`UnwrapResponsive<T>`})}),(0,c.jsx)(t.td,{children:`conditional type, извлекающий inner value`})]}),(0,c.jsxs)(t.tr,{children:[(0,c.jsx)(t.td,{children:(0,c.jsx)(t.code,{children:`ResolvedProps<T>`})}),(0,c.jsxs)(t.td,{children:[`mapped type с `,(0,c.jsx)(t.code,{children:`UnwrapResponsive`}),` для каждого поля`]})]})]})]}),`
|
|
1249
|
+
`,(0,c.jsxs)(t.p,{children:[`Options `,(0,c.jsx)(t.code,{children:`useMediaQuery`}),` и snapshot `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),` видны в сигнатурах, но не экспортируются отдельными type aliases. Используйте inline object type или `,(0,c.jsx)(t.code,{children:`ReturnType<typeof useMatchMedia>`}),`.`]}),`
|
|
1250
|
+
`,(0,c.jsx)(t.h2,{id:`вопросы-и-ответы`,children:`Вопросы и ответы`}),`
|
|
1251
|
+
`,(0,c.jsx)(t.h3,{id:`можно-ли-использовать-pure-helpers-без-react`,children:`Можно ли использовать pure helpers без React?`}),`
|
|
1252
|
+
`,(0,c.jsxs)(t.p,{children:[`Да, они не вызывают React. Но entrypoint содержит React hooks, поэтому package resolver host-приложения должен разрешить peer dependency `,(0,c.jsx)(t.code,{children:`react`}),`.`]}),`
|
|
1253
|
+
`,(0,c.jsxs)(t.h3,{id:`можно-ли-вызвать-usemediaquery-в-обычной-функции`,children:[`Можно ли вызвать `,(0,c.jsx)(t.code,{children:`useMediaQuery`}),` в обычной функции?`]}),`
|
|
1254
|
+
`,(0,c.jsx)(t.p,{children:`Нет. Это React hook. Создайте custom hook или передайте уже вычисленный boolean.`}),`
|
|
1255
|
+
`,(0,c.jsx)(t.h3,{id:`почему-server-breakpoint--laptop`,children:`Почему server breakpoint — laptop?`}),`
|
|
1256
|
+
`,(0,c.jsx)(t.p,{children:`Сервер не знает viewport. Default snapshot имеет все matches false, а fallback алгоритма выбирает laptop.`}),`
|
|
1257
|
+
`,(0,c.jsxs)(t.h3,{id:`можно-ли-настроить-ssr-fallback-у-usematchmedia`,children:[`Можно ли настроить SSR fallback у `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),`?`]}),`
|
|
1258
|
+
`,(0,c.jsxs)(t.p,{children:[`Нет. Настраиваемый fallback есть только у `,(0,c.jsx)(t.code,{children:`useMediaQuery`}),`.`]}),`
|
|
1259
|
+
`,(0,c.jsx)(t.h3,{id:`можно-ли-добавить-четвёртый-breakpoint`,children:`Можно ли добавить четвёртый breakpoint?`}),`
|
|
1260
|
+
`,(0,c.jsxs)(t.p,{children:[`Через публичный API — нет. Используйте `,(0,c.jsx)(t.code,{children:`useMediaQuery`}),` либо расширяйте общий module contract, если новый breakpoint нужен всему пакету.`]}),`
|
|
1261
|
+
`,(0,c.jsxs)(t.h3,{id:`почему-breakpoints_em-если-query-строятся-в-px`,children:[`Почему `,(0,c.jsx)(t.code,{children:`BREAKPOINTS_EM`}),`, если query строятся в px?`]}),`
|
|
1262
|
+
`,(0,c.jsxs)(t.p,{children:[`Границы описаны относительно базового размера текста. `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),` читает root font-size и конвертирует их в px перед `,(0,c.jsx)(t.code,{children:`matchMedia`}),`.`]}),`
|
|
1263
|
+
`,(0,c.jsxs)(t.h3,{id:`можно-ли-использовать-breakpoints_em-в-css`,children:[`Можно ли использовать `,(0,c.jsx)(t.code,{children:`BREAKPOINTS_EM`}),` в CSS?`]}),`
|
|
1264
|
+
`,(0,c.jsx)(t.p,{children:`TypeScript constant нельзя напрямую прочитать из статического CSS. Не копируйте числа в десятки мест; используйте единый styling build contract или design tokens проекта.`}),`
|
|
1265
|
+
`,(0,c.jsxs)(t.h3,{id:`возвращает-ли-pxtoem-css-unit`,children:[`Возвращает ли `,(0,c.jsx)(t.code,{children:`pxToEm`}),` CSS unit?`]}),`
|
|
1266
|
+
`,(0,c.jsx)(t.p,{children:`Нет. Она возвращает только числовую строку.`}),`
|
|
1267
|
+
`,(0,c.jsxs)(t.h3,{id:`что-произойдёт-с-null`,children:[`Что произойдёт с `,(0,c.jsx)(t.code,{children:`null`}),`?`]}),`
|
|
1268
|
+
`,(0,c.jsxs)(t.p,{children:[`Top-level `,(0,c.jsx)(t.code,{children:`null`}),` может вернуться как обычное value. `,(0,c.jsx)(t.code,{children:`null`}),` внутри responsive map считается отсутствующим и запускает fallback.`]}),`
|
|
1269
|
+
`,(0,c.jsxs)(t.h3,{id:`нужно-ли-memoize-результат-usematchmedia`,children:[`Нужно ли memoize результат `,(0,c.jsx)(t.code,{children:`useMatchMedia`}),`?`]}),`
|
|
1270
|
+
`,(0,c.jsx)(t.p,{children:`Нет. Store сохраняет snapshot reference, пока matches не изменились. Memoize может понадобиться для object, который consumer создаёт на его основе.`}),`
|
|
1271
|
+
`,(0,c.jsx)(t.h3,{id:`hook-отслеживает-browser-zoom`,children:`Hook отслеживает browser zoom?`}),`
|
|
1272
|
+
`,(0,c.jsxs)(t.p,{children:[`Store реагирует на фактическое изменение root `,(0,c.jsx)(t.code,{children:`em`}),` через observers. Конкретное влияние zoom на CSS pixels и computed font-size зависит от browser.`]}),`
|
|
1273
|
+
`,(0,c.jsx)(t.h3,{id:`что-выбрать-для-адаптивной-карточки`,children:`Что выбрать для адаптивной карточки?`}),`
|
|
1274
|
+
`,(0,c.jsxs)(t.p,{children:[`Сначала проверьте готовые Flex/Grid/layout primitives в `,(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-ui`}),`. Низкоуровневый `,(0,c.jsx)(t.code,{children:`media`}),` нужен при создании общего primitive или когда готового контракта недостаточно.`]}),`
|
|
1275
|
+
`,(0,c.jsx)(t.h2,{id:`связанные-документы`,children:`Связанные документы`}),`
|
|
1276
|
+
`,(0,c.jsxs)(t.ul,{children:[`
|
|
1277
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`../foundationLib.mdx`,children:`Обзор foundation-lib`})}),`
|
|
1278
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`../../README.md`,children:`Корневой README пакета`})}),`
|
|
1279
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`../dom/README.mdx`,children:`DOM helpers`})}),`
|
|
1280
|
+
`,(0,c.jsx)(t.li,{children:(0,c.jsx)(t.a,{href:`../hooks/README.mdx`,children:`Общие React hooks`})}),`
|
|
1281
|
+
`]}),`
|
|
1282
|
+
`,(0,c.jsxs)(t.p,{children:[`Исходный публичный entrypoint — `,(0,c.jsx)(t.code,{children:`@ryuzaki13/react-foundation-lib/media`}),`. Внутренние файлы модуля не являются отдельными package subpaths.`]})]})}function s(e={}){let{wrapper:t}={...n(),...e.components};return t?(0,c.jsx)(t,{...e,children:(0,c.jsx)(o,{...e})}):o(e)}var c;function l(){return(l=e((()=>{c=t(),r(),a()})))()}l();export{s as default};
|