@janga/norna 0.7.12 → 0.7.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/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
- package/examples/feature-demos/media-and-surfaces/site/config.md +0 -1
- package/package.json +1 -1
- package/scripts/lib/project-config.mjs +1 -1
- package/scripts/test-client-javascript.mjs +0 -15
- package/scripts/test-project-config.mjs +1 -6
- package/src/components/SiteNavigation.astro +1 -5
- package/src/components/NornaSmoothScrollScript.astro +0 -250
|
@@ -1 +1 @@
|
|
|
1
|
-
[["Map",1,2,9,10,91,92,112,113],"meta::meta",["Map",3,4,5,6,7,8],"astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":\"jsx\",\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":\"127.0.0.1\",\"port\":59704,\"allowedHosts\":[\"127.0.0.1\",\"127.0.0.1\",\"127.0.0.1\",\"127.0.0.1\"]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"dangerouslyProcessSVG\":false,\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"processor\":{\"name\":\"satteri\",\"options\":{\"mdastPlugins\":[],\"hastPlugins\":[],\"features\":{}}}},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[],\"csp\":false,\"actionBodySizeLimit\":1048576,\"serverIslandBodySizeLimit\":1048576},\"env\":{\"schema\":{},\"validateSecrets\":false},\"prerenderConflictBehavior\":\"warn\",\"fetchFile\":\"fetch\",\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"chromeDevtoolsWorkspace\":false,\"collectionStorage\":\"single-file\"},\"legacy\":{\"collectionsBackwardsCompat\":false}}","astro-version","7.1.3","content-config-digest","ef24ddbc7d731e37","site",["Map",11,12,35,36,63,64],"site-content",{"id":11,"data":13,"body":19,"filePath":20,"digest":21,"rendered":22},{"title":14,"description":15,"navigation":16,"sections":18},"Media and Surfaces","A visual test bench for Norna's managed media and presentation surfaces.",{"label":17},"Home",{},"## Media and surfaces {#intro}\n\nThis site is a focused test bench for Norna's managed media blocks, sidenotes,\npalettes and section-surface controls.\n\nUse [Media blocks](/media/) to compare image stacks, carousels, card lists and\nsidenotes. Use [Surfaces](/surfaces/) to inspect palettes and automatic section\nsurface sequences.\n\nSite identity, banners and footers have their own\n[sitewide content demo](https://janga.github.io/norna/examples/feature-demos/sitewide-content/).","site/content.md","78ec072eba5fb94d",{"html":23,"metadata":24},"\u003Ch2 id=\"media-and-surfaces-intro\">Media and surfaces {#intro}\u003C/h2>\n\u003Cp>This site is a focused test bench for Norna’s managed media blocks, sidenotes,\npalettes and section-surface controls.\u003C/p>\n\u003Cp>Use \u003Ca href=\"/media/\">Media blocks\u003C/a> to compare image stacks, carousels, card lists and\nsidenotes. Use \u003Ca href=\"/surfaces/\">Surfaces\u003C/a> to inspect palettes and automatic section\nsurface sequences.\u003C/p>\n\u003Cp>Site identity, banners and footers have their own\n\u003Ca href=\"https://janga.github.io/norna/examples/feature-demos/sitewide-content/\">sitewide content demo\u003C/a>.\u003C/p>\n",{"headings":25,"localImagePaths":30,"remoteImagePaths":31,"frontmatter":32,"imagePaths":34},[26],{"depth":27,"slug":28,"text":29},2,"media-and-surfaces-intro","Media and surfaces {#intro}",[],[],{"title":14,"description":15,"navigation":33},{"label":17},[],"site-route-010-media",{"id":35,"data":37,"body":42,"filePath":43,"digest":44,"rendered":45},{"title":38,"description":39,"navigation":40,"sections":41},"Media blocks","Compare Norna's managed image stacks, carousels and card lists.",{"label":38},{},"## Image stack {#stack}\n\nAn image stack places one or more managed images in the normal Markdown flow.\nEach source file belongs in the section's image folder, and Norna creates the\nresponsive output used by the browser.{note-ref}\n\n{note: Local image references are validated against the image folder for this section before the site is built.}\n\n```norna-image-stack\n- image: stack-one.svg\n alt: A pale panel with a single large circle.\n caption: One image can introduce a visual idea.\n- image: stack-two.svg\n alt: A dark panel with three aligned blocks.\n caption: A stack can contain several related images.\n```\n\n## Image carousel {#carousel}\n\nA carousel keeps a related group together while showing one image at a time.\nThe controls, keyboard interaction and current position are provided by\nNorna.{note-ref}\n\n{note: The carousel provides controls, keyboard interaction and readable position status without route-specific components.}\n\n```norna-image-carousel\n- image: carousel-one.svg\n alt: A green panel with a diagonal line.\n caption: First frame: a broad direction.\n- image: carousel-two.svg\n alt: A blue panel with two overlapping circles.\n caption: Second frame: a closer relationship.\n- image: carousel-three.svg\n alt: A red panel with a compact grid.\n caption: Third frame: a denser detail.\n```\n\n## Card list {#cards}\n\nCards are useful when several short items need the same visual treatment. The\nblock controls the layout and size without requiring a component for each\ncard.{note-ref}\n\n{note: Card layout stays within named options, so the same content remains predictable at narrow widths.}\n\n```norna-card-list\nlayout: image-top\nflow: grid\nsize: m\nwidth: normal\n\n- title: Prepare source files\n text: Keep related content and images together before building the site.\n image: card-stack.svg\n- title: Review presentation\n text: Check the result at wide and narrow browser widths.\n image: card-carousel.svg\n- title: Publish the result\n text: Build validated static output when the site is ready.\n image: card-surfaces.svg\n```\n\nEach example is an ordinary Markdown block with a bounded presentation model.","site/routes/010-media/route-content.md","05bcd9122ddb040c",{"html":46,"metadata":47},"\u003Ch2 id=\"image-stack-stack\">Image stack {#stack}\u003C/h2>\n\u003Cp>An image stack places one or more managed images in the normal Markdown flow.\nEach source file belongs in the section’s image folder, and Norna creates the\nresponsive output used by the browser.{note-ref}\u003C/p>\n\u003Cp>{note: Local image references are validated against the image folder for this section before the site is built.}\u003C/p>\n\u003Cpre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>- image: stack-one.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> alt: A pale panel with a single large circle.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> caption: One image can introduce a visual idea.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- image: stack-two.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> alt: A dark panel with three aligned blocks.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> caption: A stack can contain several related images.\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch2 id=\"image-carousel-carousel\">Image carousel {#carousel}\u003C/h2>\n\u003Cp>A carousel keeps a related group together while showing one image at a time.\nThe controls, keyboard interaction and current position are provided by\nNorna.{note-ref}\u003C/p>\n\u003Cp>{note: The carousel provides controls, keyboard interaction and readable position status without route-specific components.}\u003C/p>\n\u003Cpre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>- image: carousel-one.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> alt: A green panel with a diagonal line.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> caption: First frame: a broad direction.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- image: carousel-two.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> alt: A blue panel with two overlapping circles.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> caption: Second frame: a closer relationship.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- image: carousel-three.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> alt: A red panel with a compact grid.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> caption: Third frame: a denser detail.\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch2 id=\"card-list-cards\">Card list {#cards}\u003C/h2>\n\u003Cp>Cards are useful when several short items need the same visual treatment. The\nblock controls the layout and size without requiring a component for each\ncard.{note-ref}\u003C/p>\n\u003Cp>{note: Card layout stays within named options, so the same content remains predictable at narrow widths.}\u003C/p>\n\u003Cpre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>layout: image-top\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>flow: grid\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>size: m\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>width: normal\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- title: Prepare source files\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> text: Keep related content and images together before building the site.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> image: card-stack.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- title: Review presentation\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> text: Check the result at wide and narrow browser widths.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> image: card-carousel.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- title: Publish the result\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> text: Build validated static output when the site is ready.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> image: card-surfaces.svg\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Each example is an ordinary Markdown block with a bounded presentation model.\u003C/p>\n",{"headings":48,"localImagePaths":58,"remoteImagePaths":59,"frontmatter":60,"imagePaths":62},[49,52,55],{"depth":27,"slug":50,"text":51},"image-stack-stack","Image stack {#stack}",{"depth":27,"slug":53,"text":54},"image-carousel-carousel","Image carousel {#carousel}",{"depth":27,"slug":56,"text":57},"card-list-cards","Card list {#cards}",[],[],{"title":38,"description":39,"navigation":61},{"label":38},[],"site-route-020-surfaces",{"id":63,"data":65,"body":70,"filePath":71,"digest":72,"rendered":73},{"title":66,"description":67,"navigation":68,"sections":69},"Surfaces","See how palette defaults and section surfaces work together.",{"label":66},{},"## Palette options {#palette-options}\n\nNorna provides a small set of named palettes. The theme chooses one\nfor the whole site:\n\n```yaml\npresentation:\n palette: dark\n```\n\nThe available choices are `dark`, `light` and `paper`. A palette defines the\npage, navigation, frame and section colours together, so these parts stay in\nbalance.\n\n## Emphasis surface {#emphasis-surface}\n\nThe route theme uses a palette and a surface sequence when a visual pause or\nchange of rhythm helps the reader. This is not a free-form colour override:\n\n```yaml\npresentation:\n sectionSurfaces:\n mode: cycle\n sequence: [base, soft, emphasis]\n```\n\nThe theme can cycle through the selected surfaces automatically. Individual\nsections do not select their own colours.\n\n## A practical rule {#practical-rule}\n\nUse the site palette for the overall tone. Use soft and emphasis surfaces for a\nsmall number of meaningful transitions, not to make every section look\nunrelated.\n\nTypography presets and media blocks can vary the content inside that system;\nsurfaces provide the route-level rhythm around it.","site/routes/020-surfaces/route-content.md","0566cc76e3c9f696",{"html":74,"metadata":75},"\u003Ch2 id=\"palette-options-palette-options\">Palette options {#palette-options}\u003C/h2>\n\u003Cp>Norna provides a small set of named palettes. The theme chooses one\nfor the whole site:\u003C/p>\n\u003Cpre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"yaml\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">presentation\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\"> palette\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">dark\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>The available choices are \u003Ccode>dark\u003C/code>, \u003Ccode>light\u003C/code> and \u003Ccode>paper\u003C/code>. A palette defines the\npage, navigation, frame and section colours together, so these parts stay in\nbalance.\u003C/p>\n\u003Ch2 id=\"emphasis-surface-emphasis-surface\">Emphasis surface {#emphasis-surface}\u003C/h2>\n\u003Cp>The route theme uses a palette and a surface sequence when a visual pause or\nchange of rhythm helps the reader. This is not a free-form colour override:\u003C/p>\n\u003Cpre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"yaml\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">presentation\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\"> sectionSurfaces\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\"> mode\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">cycle\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\"> sequence\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">base\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">soft\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">emphasis\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>The theme can cycle through the selected surfaces automatically. Individual\nsections do not select their own colours.\u003C/p>\n\u003Ch2 id=\"a-practical-rule-practical-rule\">A practical rule {#practical-rule}\u003C/h2>\n\u003Cp>Use the site palette for the overall tone. Use soft and emphasis surfaces for a\nsmall number of meaningful transitions, not to make every section look\nunrelated.\u003C/p>\n\u003Cp>Typography presets and media blocks can vary the content inside that system;\nsurfaces provide the route-level rhythm around it.\u003C/p>\n",{"headings":76,"localImagePaths":86,"remoteImagePaths":87,"frontmatter":88,"imagePaths":90},[77,80,83],{"depth":27,"slug":78,"text":79},"palette-options-palette-options","Palette options {#palette-options}",{"depth":27,"slug":81,"text":82},"emphasis-surface-emphasis-surface","Emphasis surface {#emphasis-surface}",{"depth":27,"slug":84,"text":85},"a-practical-rule-practical-rule","A practical rule {#practical-rule}",[],[],{"title":66,"description":67,"navigation":89},{"label":66},[],"sitewide",["Map",93,94],"site-sitewide",{"id":93,"data":95,"filePath":100,"digest":101,"rendered":102},{"navigation":96,"banners":97,"footer":98},{"brand":14},[],{"copyrightMessage":99},"Norna media and surfaces example.","site/sitewide-content.md","a62822e81bc10bcf",{"html":103,"metadata":104},"",{"headings":105,"localImagePaths":106,"remoteImagePaths":107,"frontmatter":108,"imagePaths":111},[],[],[],{"navigation":109,"footer":110},{"brand":14},{"copyrightMessage":99},[],"theme",["Map",114,115],"theme.md",{"id":114,"data":116,"filePath":118,"digest":119,"rendered":120},{"preset":117},"project","site/theme.md","d19ab283a10fb060",{"html":103,"metadata":121},{"headings":122,"localImagePaths":123,"remoteImagePaths":124,"frontmatter":125,"imagePaths":126},[],[],[],{"preset":117},[]]
|
|
1
|
+
[["Map",1,2,9,10,91,92,112,113],"meta::meta",["Map",3,4,5,6,7,8],"astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":\"jsx\",\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":\"127.0.0.1\",\"port\":60863,\"allowedHosts\":[\"127.0.0.1\",\"127.0.0.1\",\"127.0.0.1\",\"127.0.0.1\"]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"dangerouslyProcessSVG\":false,\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"processor\":{\"name\":\"satteri\",\"options\":{\"mdastPlugins\":[],\"hastPlugins\":[],\"features\":{}}}},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[],\"csp\":false,\"actionBodySizeLimit\":1048576,\"serverIslandBodySizeLimit\":1048576},\"env\":{\"schema\":{},\"validateSecrets\":false},\"prerenderConflictBehavior\":\"warn\",\"fetchFile\":\"fetch\",\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"chromeDevtoolsWorkspace\":false,\"collectionStorage\":\"single-file\"},\"legacy\":{\"collectionsBackwardsCompat\":false}}","astro-version","7.1.3","content-config-digest","ef24ddbc7d731e37","site",["Map",11,12,35,36,63,64],"site-content",{"id":11,"data":13,"body":19,"filePath":20,"digest":21,"rendered":22},{"title":14,"description":15,"navigation":16,"sections":18},"Media and Surfaces","A visual test bench for Norna's managed media and presentation surfaces.",{"label":17},"Home",{},"## Media and surfaces {#intro}\n\nThis site is a focused test bench for Norna's managed media blocks, sidenotes,\npalettes and section-surface controls.\n\nUse [Media blocks](/media/) to compare image stacks, carousels, card lists and\nsidenotes. Use [Surfaces](/surfaces/) to inspect palettes and automatic section\nsurface sequences.\n\nSite identity, banners and footers have their own\n[sitewide content demo](https://janga.github.io/norna/examples/feature-demos/sitewide-content/).","site/content.md","78ec072eba5fb94d",{"html":23,"metadata":24},"\u003Ch2 id=\"media-and-surfaces-intro\">Media and surfaces {#intro}\u003C/h2>\n\u003Cp>This site is a focused test bench for Norna’s managed media blocks, sidenotes,\npalettes and section-surface controls.\u003C/p>\n\u003Cp>Use \u003Ca href=\"/media/\">Media blocks\u003C/a> to compare image stacks, carousels, card lists and\nsidenotes. Use \u003Ca href=\"/surfaces/\">Surfaces\u003C/a> to inspect palettes and automatic section\nsurface sequences.\u003C/p>\n\u003Cp>Site identity, banners and footers have their own\n\u003Ca href=\"https://janga.github.io/norna/examples/feature-demos/sitewide-content/\">sitewide content demo\u003C/a>.\u003C/p>\n",{"headings":25,"localImagePaths":30,"remoteImagePaths":31,"frontmatter":32,"imagePaths":34},[26],{"depth":27,"slug":28,"text":29},2,"media-and-surfaces-intro","Media and surfaces {#intro}",[],[],{"title":14,"description":15,"navigation":33},{"label":17},[],"site-route-010-media",{"id":35,"data":37,"body":42,"filePath":43,"digest":44,"rendered":45},{"title":38,"description":39,"navigation":40,"sections":41},"Media blocks","Compare Norna's managed image stacks, carousels and card lists.",{"label":38},{},"## Image stack {#stack}\n\nAn image stack places one or more managed images in the normal Markdown flow.\nEach source file belongs in the section's image folder, and Norna creates the\nresponsive output used by the browser.{note-ref}\n\n{note: Local image references are validated against the image folder for this section before the site is built.}\n\n```norna-image-stack\n- image: stack-one.svg\n alt: A pale panel with a single large circle.\n caption: One image can introduce a visual idea.\n- image: stack-two.svg\n alt: A dark panel with three aligned blocks.\n caption: A stack can contain several related images.\n```\n\n## Image carousel {#carousel}\n\nA carousel keeps a related group together while showing one image at a time.\nThe controls, keyboard interaction and current position are provided by\nNorna.{note-ref}\n\n{note: The carousel provides controls, keyboard interaction and readable position status without route-specific components.}\n\n```norna-image-carousel\n- image: carousel-one.svg\n alt: A green panel with a diagonal line.\n caption: First frame: a broad direction.\n- image: carousel-two.svg\n alt: A blue panel with two overlapping circles.\n caption: Second frame: a closer relationship.\n- image: carousel-three.svg\n alt: A red panel with a compact grid.\n caption: Third frame: a denser detail.\n```\n\n## Card list {#cards}\n\nCards are useful when several short items need the same visual treatment. The\nblock controls the layout and size without requiring a component for each\ncard.{note-ref}\n\n{note: Card layout stays within named options, so the same content remains predictable at narrow widths.}\n\n```norna-card-list\nlayout: image-top\nflow: grid\nsize: m\nwidth: normal\n\n- title: Prepare source files\n text: Keep related content and images together before building the site.\n image: card-stack.svg\n- title: Review presentation\n text: Check the result at wide and narrow browser widths.\n image: card-carousel.svg\n- title: Publish the result\n text: Build validated static output when the site is ready.\n image: card-surfaces.svg\n```\n\nEach example is an ordinary Markdown block with a bounded presentation model.","site/routes/010-media/route-content.md","05bcd9122ddb040c",{"html":46,"metadata":47},"\u003Ch2 id=\"image-stack-stack\">Image stack {#stack}\u003C/h2>\n\u003Cp>An image stack places one or more managed images in the normal Markdown flow.\nEach source file belongs in the section’s image folder, and Norna creates the\nresponsive output used by the browser.{note-ref}\u003C/p>\n\u003Cp>{note: Local image references are validated against the image folder for this section before the site is built.}\u003C/p>\n\u003Cpre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>- image: stack-one.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> alt: A pale panel with a single large circle.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> caption: One image can introduce a visual idea.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- image: stack-two.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> alt: A dark panel with three aligned blocks.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> caption: A stack can contain several related images.\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch2 id=\"image-carousel-carousel\">Image carousel {#carousel}\u003C/h2>\n\u003Cp>A carousel keeps a related group together while showing one image at a time.\nThe controls, keyboard interaction and current position are provided by\nNorna.{note-ref}\u003C/p>\n\u003Cp>{note: The carousel provides controls, keyboard interaction and readable position status without route-specific components.}\u003C/p>\n\u003Cpre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>- image: carousel-one.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> alt: A green panel with a diagonal line.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> caption: First frame: a broad direction.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- image: carousel-two.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> alt: A blue panel with two overlapping circles.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> caption: Second frame: a closer relationship.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- image: carousel-three.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> alt: A red panel with a compact grid.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> caption: Third frame: a denser detail.\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch2 id=\"card-list-cards\">Card list {#cards}\u003C/h2>\n\u003Cp>Cards are useful when several short items need the same visual treatment. The\nblock controls the layout and size without requiring a component for each\ncard.{note-ref}\u003C/p>\n\u003Cp>{note: Card layout stays within named options, so the same content remains predictable at narrow widths.}\u003C/p>\n\u003Cpre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>layout: image-top\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>flow: grid\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>size: m\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>width: normal\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- title: Prepare source files\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> text: Keep related content and images together before building the site.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> image: card-stack.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- title: Review presentation\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> text: Check the result at wide and narrow browser widths.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> image: card-carousel.svg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>- title: Publish the result\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> text: Build validated static output when the site is ready.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan> image: card-surfaces.svg\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Each example is an ordinary Markdown block with a bounded presentation model.\u003C/p>\n",{"headings":48,"localImagePaths":58,"remoteImagePaths":59,"frontmatter":60,"imagePaths":62},[49,52,55],{"depth":27,"slug":50,"text":51},"image-stack-stack","Image stack {#stack}",{"depth":27,"slug":53,"text":54},"image-carousel-carousel","Image carousel {#carousel}",{"depth":27,"slug":56,"text":57},"card-list-cards","Card list {#cards}",[],[],{"title":38,"description":39,"navigation":61},{"label":38},[],"site-route-020-surfaces",{"id":63,"data":65,"body":70,"filePath":71,"digest":72,"rendered":73},{"title":66,"description":67,"navigation":68,"sections":69},"Surfaces","See how palette defaults and section surfaces work together.",{"label":66},{},"## Palette options {#palette-options}\n\nNorna provides a small set of named palettes. The theme chooses one\nfor the whole site:\n\n```yaml\npresentation:\n palette: dark\n```\n\nThe available choices are `dark`, `light` and `paper`. A palette defines the\npage, navigation, frame and section colours together, so these parts stay in\nbalance.\n\n## Emphasis surface {#emphasis-surface}\n\nThe route theme uses a palette and a surface sequence when a visual pause or\nchange of rhythm helps the reader. This is not a free-form colour override:\n\n```yaml\npresentation:\n sectionSurfaces:\n mode: cycle\n sequence: [base, soft, emphasis]\n```\n\nThe theme can cycle through the selected surfaces automatically. Individual\nsections do not select their own colours.\n\n## A practical rule {#practical-rule}\n\nUse the site palette for the overall tone. Use soft and emphasis surfaces for a\nsmall number of meaningful transitions, not to make every section look\nunrelated.\n\nTypography presets and media blocks can vary the content inside that system;\nsurfaces provide the route-level rhythm around it.","site/routes/020-surfaces/route-content.md","0566cc76e3c9f696",{"html":74,"metadata":75},"\u003Ch2 id=\"palette-options-palette-options\">Palette options {#palette-options}\u003C/h2>\n\u003Cp>Norna provides a small set of named palettes. The theme chooses one\nfor the whole site:\u003C/p>\n\u003Cpre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"yaml\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">presentation\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\"> palette\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">dark\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>The available choices are \u003Ccode>dark\u003C/code>, \u003Ccode>light\u003C/code> and \u003Ccode>paper\u003C/code>. A palette defines the\npage, navigation, frame and section colours together, so these parts stay in\nbalance.\u003C/p>\n\u003Ch2 id=\"emphasis-surface-emphasis-surface\">Emphasis surface {#emphasis-surface}\u003C/h2>\n\u003Cp>The route theme uses a palette and a surface sequence when a visual pause or\nchange of rhythm helps the reader. This is not a free-form colour override:\u003C/p>\n\u003Cpre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"yaml\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">presentation\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\"> sectionSurfaces\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\"> mode\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">cycle\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\"> sequence\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">base\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">soft\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">emphasis\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>The theme can cycle through the selected surfaces automatically. Individual\nsections do not select their own colours.\u003C/p>\n\u003Ch2 id=\"a-practical-rule-practical-rule\">A practical rule {#practical-rule}\u003C/h2>\n\u003Cp>Use the site palette for the overall tone. Use soft and emphasis surfaces for a\nsmall number of meaningful transitions, not to make every section look\nunrelated.\u003C/p>\n\u003Cp>Typography presets and media blocks can vary the content inside that system;\nsurfaces provide the route-level rhythm around it.\u003C/p>\n",{"headings":76,"localImagePaths":86,"remoteImagePaths":87,"frontmatter":88,"imagePaths":90},[77,80,83],{"depth":27,"slug":78,"text":79},"palette-options-palette-options","Palette options {#palette-options}",{"depth":27,"slug":81,"text":82},"emphasis-surface-emphasis-surface","Emphasis surface {#emphasis-surface}",{"depth":27,"slug":84,"text":85},"a-practical-rule-practical-rule","A practical rule {#practical-rule}",[],[],{"title":66,"description":67,"navigation":89},{"label":66},[],"sitewide",["Map",93,94],"site-sitewide",{"id":93,"data":95,"filePath":100,"digest":101,"rendered":102},{"navigation":96,"banners":97,"footer":98},{"brand":14},[],{"copyrightMessage":99},"Norna media and surfaces example.","site/sitewide-content.md","a62822e81bc10bcf",{"html":103,"metadata":104},"",{"headings":105,"localImagePaths":106,"remoteImagePaths":107,"frontmatter":108,"imagePaths":111},[],[],[],{"navigation":109,"footer":110},{"brand":14},{"copyrightMessage":99},[],"theme",["Map",114,115],"theme.md",{"id":114,"data":116,"filePath":118,"digest":119,"rendered":120},{"preset":117},"project","site/theme.md","d19ab283a10fb060",{"html":103,"metadata":121},{"headings":122,"localImagePaths":123,"remoteImagePaths":124,"frontmatter":125,"imagePaths":126},[],[],[],{"preset":117},[]]
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@janga/norna",
|
|
3
3
|
"description": "An opinionated CLI for building responsive websites from Markdown, images, and theme presets.",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.13",
|
|
6
6
|
"license": "GPL-3.0-only",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -249,7 +249,7 @@ const readLocale = (config) => {
|
|
|
249
249
|
const rawConfig = assertObject(siteConfig, 'config frontmatter');
|
|
250
250
|
const rawTheme = assertObject(themeConfig, 'theme frontmatter', siteThemeLabel);
|
|
251
251
|
const siteUrl = readSiteUrl(rawConfig);
|
|
252
|
-
const scrollBehaviorNames = ['instant', 'browser-smooth'
|
|
252
|
+
const scrollBehaviorNames = ['instant', 'browser-smooth'];
|
|
253
253
|
|
|
254
254
|
const defaultFontFamily = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
|
|
255
255
|
const layoutDensityNames = ['compact', 'normal', 'airy'];
|
|
@@ -100,21 +100,6 @@ Additional sections do not make the page interactive.
|
|
|
100
100
|
assertNoClientJavaScript(browserSmoothHtml, 'The browser smooth scroll mode');
|
|
101
101
|
assertScrollBehavior(browserSmoothHtml, 'smooth', 'The browser smooth scroll mode');
|
|
102
102
|
|
|
103
|
-
await writeConfig('norna-smooth');
|
|
104
|
-
runBuild();
|
|
105
|
-
const nornaSmoothHtml = await readPage();
|
|
106
|
-
assert.equal(
|
|
107
|
-
getScripts(nornaSmoothHtml).length,
|
|
108
|
-
1,
|
|
109
|
-
'The Norna smooth scroll mode should load only its scroll implementation.',
|
|
110
|
-
);
|
|
111
|
-
assertScrollBehavior(nornaSmoothHtml, 'auto', 'The Norna smooth scroll mode');
|
|
112
|
-
assert.match(
|
|
113
|
-
getScripts(nornaSmoothHtml)[0],
|
|
114
|
-
/nornaScrollActive/,
|
|
115
|
-
'The Norna smooth scroll mode should emit the Norna scroll implementation.',
|
|
116
|
-
);
|
|
117
|
-
|
|
118
103
|
await writeConfig();
|
|
119
104
|
|
|
120
105
|
await mkdir(path.join(routeDir, 'images', 'details'), { recursive: true });
|
|
@@ -77,11 +77,6 @@ try {
|
|
|
77
77
|
assert.equal(localizedConfig.labels.skipToContent, 'Hoppa till innehållet');
|
|
78
78
|
assert.equal(localizedConfig.scrollBehavior, 'browser-smooth');
|
|
79
79
|
|
|
80
|
-
const nornaSmoothSite = await createSite('norna-smooth', '---\nurl: https://example.com/\nscrollBehavior: norna-smooth\n---\n');
|
|
81
|
-
const nornaSmoothResult = loadConfig(nornaSmoothSite);
|
|
82
|
-
assert.equal(nornaSmoothResult.status, 0, nornaSmoothResult.stderr);
|
|
83
|
-
assert.equal(JSON.parse(nornaSmoothResult.stdout).scrollBehavior, 'norna-smooth');
|
|
84
|
-
|
|
85
80
|
const overrideResult = loadConfig(minimalSite, {
|
|
86
81
|
NORNA_SITE_URL: 'http://127.0.0.1:4567/preview',
|
|
87
82
|
});
|
|
@@ -113,7 +108,7 @@ try {
|
|
|
113
108
|
);
|
|
114
109
|
assertFailure(
|
|
115
110
|
loadConfig(await createSite('invalid-scroll-behavior', '---\nurl: https://example.com/\nscrollBehavior: slow\n---\n')),
|
|
116
|
-
/scrollBehavior must be one of instant, browser-smooth
|
|
111
|
+
/scrollBehavior must be one of instant, browser-smooth/,
|
|
117
112
|
);
|
|
118
113
|
assertFailure(
|
|
119
114
|
loadConfig(await createSite('obsolete-smooth-scroll', '---\nurl: https://example.com/\nsmoothScroll: true\n---\n')),
|
|
@@ -4,7 +4,6 @@ import { withBasePath } from '../lib/basePath';
|
|
|
4
4
|
import type { SectionNavigation } from '../lib/sectionContent';
|
|
5
5
|
import type { SitePage } from '../lib/sitePages';
|
|
6
6
|
import MobileSectionNavigationScript from './MobileSectionNavigationScript.astro';
|
|
7
|
-
import NornaSmoothScrollScript from './NornaSmoothScrollScript.astro';
|
|
8
7
|
|
|
9
8
|
type FrameColors = {
|
|
10
9
|
backgroundColor: string;
|
|
@@ -49,7 +48,6 @@ const showSinglePageSectionNavigation = Boolean(
|
|
|
49
48
|
const hasCurrentPageSectionMenu = Boolean(
|
|
50
49
|
showSiteNavigation && currentPageNavigation && hasSectionMenu(currentPageNavigation.sections),
|
|
51
50
|
);
|
|
52
|
-
const useNornaSmoothScroll = projectConfig.navigation.scrollBehavior === 'norna-smooth';
|
|
53
51
|
---
|
|
54
52
|
|
|
55
53
|
<a class="skip-link" href="#main-content">{projectConfig.locale.labels.skipToContent}</a>
|
|
@@ -142,6 +140,4 @@ const useNornaSmoothScroll = projectConfig.navigation.scrollBehavior === 'norna-
|
|
|
142
140
|
|
|
143
141
|
</header>
|
|
144
142
|
|
|
145
|
-
{
|
|
146
|
-
? <NornaSmoothScrollScript />
|
|
147
|
-
: hasCurrentPageSectionMenu && <MobileSectionNavigationScript />}
|
|
143
|
+
{hasCurrentPageSectionMenu && <MobileSectionNavigationScript />}
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
const siteTop = document.querySelector<HTMLElement>('.site-top');
|
|
3
|
-
const sections = Array.from(document.querySelectorAll<HTMLElement>('.site-section'));
|
|
4
|
-
const links = Array.from(document.querySelectorAll<HTMLAnchorElement>('.site-nav-submenu a, .mobile-page-nav a, .page-nav a'));
|
|
5
|
-
const mobileMenu = document.querySelector<HTMLDetailsElement>('.mobile-nav-menu');
|
|
6
|
-
const anchorCorrectionDelays = [120, 320, 700, 1_000, 2_000];
|
|
7
|
-
const reduceMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
8
|
-
const minimumScrollDuration = 700;
|
|
9
|
-
const maximumScrollDuration = 1_800;
|
|
10
|
-
const scrollDurationPerPixel = 0.18;
|
|
11
|
-
const scrollInterruptEvents = ['wheel', 'touchstart'];
|
|
12
|
-
const scrollInterruptKeys = new Set(['ArrowDown', 'ArrowUp', 'End', 'Home', 'PageDown', 'PageUp', ' ']);
|
|
13
|
-
let activeScrollAnimation: { cancel: () => void } | null = null;
|
|
14
|
-
let anchorCorrectionToken = 0;
|
|
15
|
-
|
|
16
|
-
if ('scrollRestoration' in history) {
|
|
17
|
-
history.scrollRestoration = 'manual';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const setAnchorOffset = () => {
|
|
21
|
-
if (!siteTop) return;
|
|
22
|
-
|
|
23
|
-
const offset = Math.ceil(siteTop.getBoundingClientRect().height);
|
|
24
|
-
document.documentElement.style.setProperty('--site-top-anchor-offset', `${offset}px`);
|
|
25
|
-
return offset;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const getSectionFromHash = (hash: string) => {
|
|
29
|
-
const id = hash.replace(/^#/, '');
|
|
30
|
-
return sections.find((section) => section.id === id);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const getSectionHeading = (section?: HTMLElement) => section?.querySelector<HTMLElement>('h1, h2');
|
|
34
|
-
|
|
35
|
-
const cancelActiveScroll = () => {
|
|
36
|
-
activeScrollAnimation?.cancel();
|
|
37
|
-
activeScrollAnimation = null;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const getHashSection = () => getSectionFromHash(window.location.hash);
|
|
41
|
-
|
|
42
|
-
const setHash = (targetHash: string) => {
|
|
43
|
-
if (window.location.hash !== targetHash) {
|
|
44
|
-
history.pushState(null, '', targetHash);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const closeMobileMenu = () => {
|
|
49
|
-
if (mobileMenu) mobileMenu.open = false;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const getSectionScrollTop = (section?: HTMLElement) => {
|
|
53
|
-
const heading = getSectionHeading(section);
|
|
54
|
-
if (!heading) return;
|
|
55
|
-
|
|
56
|
-
const offset = setAnchorOffset();
|
|
57
|
-
if (offset === undefined) return;
|
|
58
|
-
|
|
59
|
-
return Math.max(0, heading.getBoundingClientRect().top + window.scrollY - offset);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const jumpTo = (top: number) => {
|
|
63
|
-
cancelActiveScroll();
|
|
64
|
-
|
|
65
|
-
const root = document.documentElement;
|
|
66
|
-
const previousScrollBehavior = root.style.scrollBehavior;
|
|
67
|
-
root.style.scrollBehavior = 'auto';
|
|
68
|
-
window.scrollTo(0, top);
|
|
69
|
-
root.style.scrollBehavior = previousScrollBehavior;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const getScrollDuration = (distance: number) => {
|
|
73
|
-
if (reduceMotionQuery.matches) return 0;
|
|
74
|
-
|
|
75
|
-
return Math.min(
|
|
76
|
-
maximumScrollDuration,
|
|
77
|
-
Math.max(minimumScrollDuration, Math.abs(distance) * scrollDurationPerPixel),
|
|
78
|
-
);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const easeInOutCubic = (progress: number) => (
|
|
82
|
-
progress < 0.5
|
|
83
|
-
? 4 * progress * progress * progress
|
|
84
|
-
: 1 - Math.pow(-2 * progress + 2, 3) / 2
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const animateScrollTo = (
|
|
88
|
-
target: number | (() => number | undefined),
|
|
89
|
-
{ duration, onComplete }: { duration?: number; onComplete?: () => void } = {},
|
|
90
|
-
) => {
|
|
91
|
-
cancelActiveScroll();
|
|
92
|
-
|
|
93
|
-
const getTop = typeof target === 'function' ? target : () => target;
|
|
94
|
-
let latestTop = getTop();
|
|
95
|
-
if (latestTop === undefined) return 0;
|
|
96
|
-
|
|
97
|
-
const startY = window.scrollY;
|
|
98
|
-
const scrollDuration = duration ?? getScrollDuration(latestTop - startY);
|
|
99
|
-
|
|
100
|
-
if (scrollDuration <= 0) {
|
|
101
|
-
const top = getTop();
|
|
102
|
-
if (top === undefined) return 0;
|
|
103
|
-
|
|
104
|
-
jumpTo(top);
|
|
105
|
-
onComplete?.();
|
|
106
|
-
return 0;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const root = document.documentElement;
|
|
110
|
-
const previousScrollBehavior = root.style.scrollBehavior;
|
|
111
|
-
const startedAt = performance.now();
|
|
112
|
-
let frameId = 0;
|
|
113
|
-
const state = { cancel: () => {} };
|
|
114
|
-
|
|
115
|
-
const cleanup = () => {
|
|
116
|
-
scrollInterruptEvents.forEach((eventName) => {
|
|
117
|
-
window.removeEventListener(eventName, interruptScroll);
|
|
118
|
-
});
|
|
119
|
-
window.removeEventListener('keydown', interruptScrollKey);
|
|
120
|
-
root.style.scrollBehavior = previousScrollBehavior;
|
|
121
|
-
delete root.dataset.nornaScrollActive;
|
|
122
|
-
|
|
123
|
-
if (activeScrollAnimation === state) {
|
|
124
|
-
activeScrollAnimation = null;
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const cancel = () => {
|
|
129
|
-
if (frameId) cancelAnimationFrame(frameId);
|
|
130
|
-
cleanup();
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const interruptScroll = () => cancel();
|
|
134
|
-
const interruptScrollKey = (event: KeyboardEvent) => {
|
|
135
|
-
if (scrollInterruptKeys.has(event.key)) interruptScroll();
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const step = (now: number) => {
|
|
139
|
-
const progress = Math.min(1, (now - startedAt) / scrollDuration);
|
|
140
|
-
const top = getTop();
|
|
141
|
-
if (top !== undefined) latestTop = top;
|
|
142
|
-
|
|
143
|
-
window.scrollTo(0, startY + (latestTop - startY) * easeInOutCubic(progress));
|
|
144
|
-
|
|
145
|
-
if (progress >= 1) {
|
|
146
|
-
window.scrollTo(0, latestTop);
|
|
147
|
-
cleanup();
|
|
148
|
-
onComplete?.();
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
frameId = requestAnimationFrame(step);
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
state.cancel = cancel;
|
|
156
|
-
activeScrollAnimation = state;
|
|
157
|
-
root.style.scrollBehavior = 'auto';
|
|
158
|
-
root.dataset.nornaScrollActive = 'true';
|
|
159
|
-
scrollInterruptEvents.forEach((eventName) => {
|
|
160
|
-
window.addEventListener(eventName, interruptScroll, { passive: true });
|
|
161
|
-
});
|
|
162
|
-
window.addEventListener('keydown', interruptScrollKey);
|
|
163
|
-
frameId = requestAnimationFrame(step);
|
|
164
|
-
|
|
165
|
-
return scrollDuration;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const scrollToSection = (
|
|
169
|
-
section: HTMLElement,
|
|
170
|
-
{ animate = true, duration, onComplete }: { animate?: boolean; duration?: number; onComplete?: () => void } = {},
|
|
171
|
-
) => {
|
|
172
|
-
const top = getSectionScrollTop(section);
|
|
173
|
-
if (top === undefined) return 0;
|
|
174
|
-
|
|
175
|
-
return animate
|
|
176
|
-
? animateScrollTo(() => getSectionScrollTop(section), { duration, onComplete })
|
|
177
|
-
: (jumpTo(top), onComplete?.(), 0);
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const correctCurrentHashAnchor = () => {
|
|
181
|
-
if (activeScrollAnimation) return;
|
|
182
|
-
|
|
183
|
-
const section = getHashSection();
|
|
184
|
-
if (section) scrollToSection(section, { animate: false });
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const scheduleHashAnchorCorrection = () => {
|
|
188
|
-
if (!window.location.hash) return;
|
|
189
|
-
|
|
190
|
-
anchorCorrectionToken += 1;
|
|
191
|
-
const token = anchorCorrectionToken;
|
|
192
|
-
|
|
193
|
-
anchorCorrectionDelays.forEach((delay) => {
|
|
194
|
-
window.setTimeout(() => {
|
|
195
|
-
if (token === anchorCorrectionToken) requestAnimationFrame(correctCurrentHashAnchor);
|
|
196
|
-
}, delay);
|
|
197
|
-
});
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const syncToLocation = ({ scrollHashlessTop = false } = {}) => {
|
|
201
|
-
const section = getHashSection();
|
|
202
|
-
|
|
203
|
-
if (section) {
|
|
204
|
-
scrollToSection(section, { animate: false });
|
|
205
|
-
scheduleHashAnchorCorrection();
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (scrollHashlessTop) jumpTo(0);
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
links.forEach((link) => {
|
|
213
|
-
link.addEventListener('click', (event) => {
|
|
214
|
-
const linkUrl = new URL(link.href, window.location.href);
|
|
215
|
-
if (linkUrl.pathname !== window.location.pathname) return;
|
|
216
|
-
|
|
217
|
-
const section = getSectionFromHash(linkUrl.hash);
|
|
218
|
-
if (!section) return;
|
|
219
|
-
|
|
220
|
-
const top = getSectionScrollTop(section);
|
|
221
|
-
if (top === undefined) return;
|
|
222
|
-
|
|
223
|
-
event.preventDefault();
|
|
224
|
-
const duration = getScrollDuration(top - window.scrollY);
|
|
225
|
-
|
|
226
|
-
setHash(linkUrl.hash);
|
|
227
|
-
closeMobileMenu();
|
|
228
|
-
if (event.detail > 0) link.blur();
|
|
229
|
-
|
|
230
|
-
scrollToSection(section, {
|
|
231
|
-
duration,
|
|
232
|
-
onComplete: scheduleHashAnchorCorrection,
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
setAnchorOffset();
|
|
238
|
-
requestAnimationFrame(() => syncToLocation());
|
|
239
|
-
|
|
240
|
-
if (siteTop && 'ResizeObserver' in window) {
|
|
241
|
-
new ResizeObserver(setAnchorOffset).observe(siteTop);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
window.addEventListener('resize', setAnchorOffset);
|
|
245
|
-
window.addEventListener('load', scheduleHashAnchorCorrection);
|
|
246
|
-
window.addEventListener('hashchange', () => syncToLocation({ scrollHashlessTop: true }));
|
|
247
|
-
window.addEventListener('popstate', () => syncToLocation({ scrollHashlessTop: true }));
|
|
248
|
-
|
|
249
|
-
window.visualViewport?.addEventListener('resize', setAnchorOffset);
|
|
250
|
-
</script>
|