diff --git a/components/ui/Accordion.tsx b/components/ui/Accordion.tsx index 375ebb9bc..7cb1fdabc 100644 --- a/components/ui/Accordion.tsx +++ b/components/ui/Accordion.tsx @@ -2,7 +2,7 @@ import { Box, Stack } from "@telegraph/layout"; import { MenuItem } from "@telegraph/menu"; import { Icon } from "@telegraph/icon"; import { AnimatePresence, motion } from "framer-motion"; -import { useState, useMemo } from "react"; +import { useState, useMemo, useLayoutEffect, useRef } from "react"; import { Text, Code } from "@telegraph/typography"; import { ChevronRight } from "lucide-react"; @@ -15,11 +15,24 @@ const AccordionGroup = ({ children }) => ( ); +function getHashFragment(): string { + if (typeof window === "undefined") return ""; + const { hash } = window.location; + if (!hash || hash === "#") return ""; + try { + return decodeURIComponent(hash.slice(1)); + } catch { + return hash.slice(1); + } +} + type AccordionProps = { children: React.ReactNode; title: string; description?: string; defaultOpen?: boolean; + /** When set, this slug is used as the element `id` and the accordion opens if the URL hash matches (for deep links). Use a URL-safe hyphenated fragment, e.g. `my-section`. */ + anchorSlug?: string; }; // Helper function to parse title and split into text and code parts @@ -68,12 +81,69 @@ const Accordion = ({ title, description, defaultOpen = false, + anchorSlug, }: AccordionProps) => { const [open, setOpen] = useState(defaultOpen); const titleParts = useMemo(() => parseTitleWithCode(title), [title]); + const elementRef = useRef(null); + + useLayoutEffect(() => { + if (!anchorSlug) return; + + let cancelled = false; + let resizeObserver: ResizeObserver | null = null; + let stopWatchingTimeoutId: number | null = null; + + const performScroll = () => { + if (cancelled) return; + elementRef.current?.scrollIntoView({ + block: "start", + behavior: "smooth", + }); + }; + + const stopWatching = () => { + if (resizeObserver) { + resizeObserver.disconnect(); + resizeObserver = null; + } + if (stopWatchingTimeoutId !== null) { + clearTimeout(stopWatchingTimeoutId); + stopWatchingTimeoutId = null; + } + }; + + const syncFromHash = () => { + if (getHashFragment() !== anchorSlug) return; + setOpen(true); + performScroll(); + + // Re-scroll whenever layout shifts (images loading, async content, etc.) + // so the accordion stays anchored to its intended position even as the + // document height changes. + stopWatching(); + if (typeof ResizeObserver !== "undefined") { + resizeObserver = new ResizeObserver(() => { + performScroll(); + }); + resizeObserver.observe(document.body); + } + // Stop correcting after layout has had time to settle so we don't + // fight subsequent user-initiated scrolls. + stopWatchingTimeoutId = window.setTimeout(stopWatching, 1500); + }; + + syncFromHash(); + window.addEventListener("hashchange", syncFromHash); + return () => { + cancelled = true; + stopWatching(); + window.removeEventListener("hashchange", syncFromHash); + }; + }, [anchorSlug]); return ( - + setOpen(!open)} diff --git a/content/integrations/chat/microsoft-teams/overview.mdx b/content/integrations/chat/microsoft-teams/overview.mdx index 1ad824271..5023ee6a8 100644 --- a/content/integrations/chat/microsoft-teams/overview.mdx +++ b/content/integrations/chat/microsoft-teams/overview.mdx @@ -163,7 +163,7 @@ To use TeamsKit, you'll need to configure the API permissions and OAuth redirect ## How to set channel data for a Microsoft Teams integration in Knock -In Knock, the [`ChannelData`](/managing-recipients/setting-channel-data) concept provides you a way of storing recipient-specific connection data for a given integration. If you reference the [channel data requirements for Microsoft Teams](/managing-recipients/setting-channel-data#chat-app-channels), you'll see that there are two different schemas for an `MsTeamsConnection` stored on a [`User`](/concepts/users) or an [`Object`](/concepts/objects) in Knock. +In Knock, the [`ChannelData`](/managing-recipients/setting-channel-data) concept provides you a way of storing recipient-specific connection data for a given integration. If you reference the [channel data requirements for Microsoft Teams](/managing-recipients/setting-channel-data#microsoft-teams-channel-data), you'll see that there are two different schemas for an `MsTeamsConnection` stored on a [`User`](/concepts/users) or an [`Object`](/concepts/objects) in Knock. Here's an example of setting channel data on an `Object` in Knock. diff --git a/content/managing-recipients/setting-channel-data.mdx b/content/managing-recipients/setting-channel-data.mdx index a2d596561..231c47823 100644 --- a/content/managing-recipients/setting-channel-data.mdx +++ b/content/managing-recipients/setting-channel-data.mdx @@ -241,7 +241,7 @@ The `PushDevice` object is used to optionally set device-level metadata for a pu | incoming_webhook.url | `string` | The Discord incoming webhook URL (to be used instead of the properties above) | - + | Property | Type | Description | | ----------- | --------------------- | ---------------------------------- | | connections | `MsTeamsConnection[]` | One or more connections to MsTeams | diff --git a/content/multi-tenancy/per-tenant-preferences.mdx b/content/multi-tenancy/per-tenant-preferences.mdx index cf037ffa9..81218a9bb 100644 --- a/content/multi-tenancy/per-tenant-preferences.mdx +++ b/content/multi-tenancy/per-tenant-preferences.mdx @@ -170,11 +170,11 @@ The Knock workflow engine uses that `tenant` parameter to evaluate the user's `P ## Tenant preference evaluation rules -Here are a few things to keep in mind when using tenant preferences. You can learn more about how preferences are merged and evaluated [here](/preferences/overview#merging-preferences). +Here are a few things to keep in mind when using tenant preferences. You can learn more about how preferences are merged and evaluated [here](/preferences/overview#tenant-specific-preference-merge-hierarchy). - When executing a workflow trigger, passing in a `tenant` will automatically load that tenant's default `PreferenceSet` (if one exists) for all recipients of the workflow. These tenant-level defaults will override a recipient's own `default` preferences. - If the recipient has any per-tenant preferences set for that `tenant.id`, they will take precedence over the tenant-level default preferences. For more information on how to override a recipient's per-tenant preferences to respect the tenant-level default preferences, see the [frequently asked questions](#frequently-asked-questions) below. -- If there is no default `PreferenceSet` on the tenant AND the recipient has no per-tenant preferences set, the recipient's `default` preferences will be used. As always, the recipient's `default` preferences are [merged](/preferences/overview#merging-preferences) with the environment-level preference defaults. +- If there is no default `PreferenceSet` on the tenant AND the recipient has no per-tenant preferences set, the recipient's `default` preferences will be used. As always, the recipient's `default` preferences are [merged](/preferences/overview#tenant-specific-preference-merge-hierarchy) with the environment-level preference defaults. ## Frequently asked questions diff --git a/content/preferences/overview.mdx b/content/preferences/overview.mdx index b8fb80417..0bb20fdab 100644 --- a/content/preferences/overview.mdx +++ b/content/preferences/overview.mdx @@ -281,7 +281,7 @@ The following hierarchies are used when merging preferences. The environment-level setting to opt all users into `collaboration` notifications will be respected because the recipient doesn't have an explicit preference for that category, but the recipient's preference to specifically opt in to SMS messages will override the environment-level setting to opt out. - + The following hierarchy is used to merge preferences when a `tenant` is applied to the workflow trigger. Each item in the list takes precedence over the ones that follow it: 1. A recipient's [tenant-specific preference](/multi-tenancy/per-tenant-preferences) set diff --git a/next-env.d.ts b/next-env.d.ts index 2d5420eba..0c7fad710 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,7 +1,7 @@ /// /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/styles/index.css b/styles/index.css index 197680698..d300cafa3 100644 --- a/styles/index.css +++ b/styles/index.css @@ -102,9 +102,9 @@ textarea:not([rows]) { min-height: 10em; } -/* Anything that has been anchored to should have extra scroll margin */ +/* Anything anchored to via #hash should clear the sticky page header */ :target { - scroll-margin-block: 5ex; + scroll-margin-top: calc(var(--header-height) + 1rem); } .dot-bg {