mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
starlight_help: Move help-beta over to starlight_help.
We are starting the cutover process and starlight_help is the directory we have agreed on to place our new help center project. We do not want to use `starlight_help` as the URL for the project, but this commit changes the url from `help-beta` to `starlight_help` temporarily since we can only change URL once we get rid of the current help center project. That will be done in a future commit.
This commit is contained in:
committed by
Tim Abbott
parent
fb49e5e420
commit
3e60b16ac1
24
starlight_help/.gitignore
vendored
Normal file
24
starlight_help/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
*.mdx
|
||||
*.md
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
478
starlight_help/astro.config.mjs
Normal file
478
starlight_help/astro.config.mjs
Normal file
@@ -0,0 +1,478 @@
|
||||
import * as fs from "node:fs";
|
||||
|
||||
import starlight from "@astrojs/starlight";
|
||||
import {defineConfig, envField} from "astro/config";
|
||||
import Icons from "unplugin-icons/vite";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
base: "starlight_help",
|
||||
vite: {
|
||||
plugins: [
|
||||
// eslint-disable-next-line new-cap
|
||||
Icons({
|
||||
compiler: "astro",
|
||||
customCollections: {
|
||||
// unplugin-icons has a FileSystemIconLoader which is more
|
||||
// versatile. But it only supports one directory path for
|
||||
// a single set of icons. We should start using that loader
|
||||
// if they add support for multiple paths in the future.
|
||||
async "zulip-icon"(iconName) {
|
||||
const sharedIconsPath = `../web/shared/icons/${iconName}.svg`;
|
||||
const webOnlyIconsPath = `../web/images/icons/${iconName}.svg`;
|
||||
|
||||
if (fs.existsSync(sharedIconsPath)) {
|
||||
return await fs.promises.readFile(sharedIconsPath, "utf8");
|
||||
} else if (fs.existsSync(webOnlyIconsPath)) {
|
||||
return await fs.promises.readFile(webOnlyIconsPath, "utf8");
|
||||
}
|
||||
throw new Error("Zulip icon not found.");
|
||||
},
|
||||
},
|
||||
iconCustomizer(collection, icon, props) {
|
||||
if (collection === "zulip-icon" || collection === "fa") {
|
||||
// We need to override some default starlight behaviour to make
|
||||
// icons look nice, see the css for this class to see the reasoning
|
||||
// for each individual override of the default css.
|
||||
props.class = "zulip-unplugin-icon";
|
||||
|
||||
if (collection === "zulip-icon" && icon.startsWith("user-circle-")) {
|
||||
const iconSuffix = icon.replace("user-circle-", "");
|
||||
props.class = `zulip-unplugin-icon user-circle user-circle-${iconSuffix}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
env: {
|
||||
schema: {
|
||||
SHOW_RELATIVE_LINKS: envField.boolean({
|
||||
context: "client",
|
||||
access: "public",
|
||||
optional: true,
|
||||
default: true,
|
||||
}),
|
||||
CORPORATE_ENABLED: envField.boolean({
|
||||
context: "client",
|
||||
access: "public",
|
||||
optional: true,
|
||||
default: true,
|
||||
}),
|
||||
SUPPORT_EMAIL: envField.string({
|
||||
context: "client",
|
||||
access: "public",
|
||||
optional: true,
|
||||
default: "zulip-admin@example.com",
|
||||
}),
|
||||
},
|
||||
},
|
||||
integrations: [
|
||||
starlight({
|
||||
title: "Zulip help center",
|
||||
components: {
|
||||
Footer: "./src/components/Footer.astro",
|
||||
Head: "./src/components/Head.astro",
|
||||
},
|
||||
pagination: false,
|
||||
routeMiddleware: "./src/route_data.ts",
|
||||
customCss: ["./src/styles/main.css"],
|
||||
sidebar: [
|
||||
{
|
||||
label: "Zulip homepage",
|
||||
link: "https://zulip.com",
|
||||
},
|
||||
{
|
||||
label: "Help center home",
|
||||
slug: "index",
|
||||
},
|
||||
{
|
||||
label: "Guides",
|
||||
items: [
|
||||
"getting-started-with-zulip",
|
||||
{
|
||||
label: "Choosing a team chat app",
|
||||
link: "https://blog.zulip.com/2024/11/04/choosing-a-team-chat-app/",
|
||||
},
|
||||
{
|
||||
label: "Why Zulip",
|
||||
link: "https://zulip.com/why-zulip/",
|
||||
},
|
||||
"trying-out-zulip",
|
||||
"zulip-cloud-or-self-hosting",
|
||||
"moving-to-zulip",
|
||||
"moderating-open-organizations",
|
||||
"setting-up-zulip-for-a-class",
|
||||
"using-zulip-for-a-class",
|
||||
"using-zulip-via-email",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Getting started",
|
||||
items: [
|
||||
"join-a-zulip-organization",
|
||||
"set-up-your-account",
|
||||
"introduction-to-topics",
|
||||
{
|
||||
label: "Starting a new topic",
|
||||
link: "/introduction-to-topics#how-to-start-a-new-topic",
|
||||
},
|
||||
"finding-a-conversation-to-read",
|
||||
"reading-conversations",
|
||||
"starting-a-new-direct-message",
|
||||
"replying-to-messages",
|
||||
"messaging-tips",
|
||||
"keyboard-shortcuts",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Setting up your organization",
|
||||
items: [
|
||||
"migrating-from-other-chat-tools",
|
||||
"create-your-organization-profile",
|
||||
"create-user-groups",
|
||||
"customize-organization-settings",
|
||||
"create-channels",
|
||||
"customize-settings-for-new-users",
|
||||
"invite-users-to-join",
|
||||
"set-up-integrations",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Account basics",
|
||||
items: [
|
||||
"edit-your-profile",
|
||||
"change-your-name",
|
||||
"change-your-email-address",
|
||||
"change-your-profile-picture",
|
||||
"change-your-password",
|
||||
"configure-email-visibility",
|
||||
"logging-in",
|
||||
"logging-out",
|
||||
"switching-between-organizations",
|
||||
"import-your-settings",
|
||||
"review-your-settings",
|
||||
"deactivate-your-account",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Preferences",
|
||||
items: [
|
||||
"dark-theme",
|
||||
"font-size",
|
||||
"line-spacing",
|
||||
"configure-send-message-keys",
|
||||
"change-your-language",
|
||||
"change-your-timezone",
|
||||
"change-the-time-format",
|
||||
"configure-emoticon-translations",
|
||||
"configure-home-view",
|
||||
"enable-full-width-display",
|
||||
"manage-your-uploaded-files",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Writing messages",
|
||||
items: [
|
||||
"format-your-message-using-markdown",
|
||||
"mention-a-user-or-group",
|
||||
"link-to-a-message-or-conversation",
|
||||
"format-a-quote",
|
||||
"quote-or-forward-a-message",
|
||||
"emoji-and-emoticons",
|
||||
"insert-a-link",
|
||||
"saved-snippets",
|
||||
"share-and-upload-files",
|
||||
"animated-gifs-from-giphy",
|
||||
"text-emphasis",
|
||||
"paragraph-and-section-formatting",
|
||||
"bulleted-lists",
|
||||
"numbered-lists",
|
||||
"tables",
|
||||
"code-blocks",
|
||||
"latex",
|
||||
"spoilers",
|
||||
"me-action-messages",
|
||||
"create-a-poll",
|
||||
"collaborative-to-do-lists",
|
||||
"global-times",
|
||||
"start-a-call",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Sending messages",
|
||||
items: [
|
||||
"open-the-compose-box",
|
||||
"mastering-the-compose-box",
|
||||
"resize-the-compose-box",
|
||||
"typing-notifications",
|
||||
"preview-your-message-before-sending",
|
||||
"verify-your-message-was-successfully-sent",
|
||||
"edit-a-message",
|
||||
"delete-a-message",
|
||||
"view-and-edit-your-message-drafts",
|
||||
"schedule-a-message",
|
||||
"message-a-channel-by-email",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Reading messages",
|
||||
items: [
|
||||
"reading-strategies",
|
||||
"inbox",
|
||||
"recent-conversations",
|
||||
"combined-feed",
|
||||
"channel-feed",
|
||||
"list-of-topics",
|
||||
"left-sidebar",
|
||||
"message-actions",
|
||||
"marking-messages-as-read",
|
||||
"marking-messages-as-unread",
|
||||
"configure-unread-message-counters",
|
||||
"configure-where-you-land",
|
||||
"emoji-reactions",
|
||||
"view-your-mentions",
|
||||
"star-a-message",
|
||||
"schedule-a-reminder",
|
||||
"view-images-and-videos",
|
||||
"view-messages-sent-by-a-user",
|
||||
"link-to-a-message-or-conversation",
|
||||
"search-for-messages",
|
||||
"printing-messages",
|
||||
"view-the-markdown-source-of-a-message",
|
||||
"view-the-exact-time-a-message-was-sent",
|
||||
"view-a-messages-edit-history",
|
||||
"collapse-a-message",
|
||||
"read-receipts",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "People",
|
||||
items: [
|
||||
"introduction-to-users",
|
||||
"user-list",
|
||||
"status-and-availability",
|
||||
"user-cards",
|
||||
"view-someones-profile",
|
||||
"direct-messages",
|
||||
"find-administrators",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Groups",
|
||||
items: ["user-groups", "view-group-members"],
|
||||
},
|
||||
{
|
||||
label: "Channels",
|
||||
items: [
|
||||
"introduction-to-channels",
|
||||
{
|
||||
label: "Subscribe to a channel",
|
||||
link: "/introduction-to-channels#browse-and-subscribe-to-channels",
|
||||
},
|
||||
"create-a-channel",
|
||||
"pin-a-channel",
|
||||
"change-the-color-of-a-channel",
|
||||
"channel-folders",
|
||||
"unsubscribe-from-a-channel",
|
||||
"manage-inactive-channels",
|
||||
"move-content-to-another-channel",
|
||||
"view-channel-information",
|
||||
"view-channel-subscribers",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Topics",
|
||||
items: [
|
||||
"introduction-to-topics",
|
||||
"rename-a-topic",
|
||||
"resolve-a-topic",
|
||||
"move-content-to-another-topic",
|
||||
"general-chat-topic",
|
||||
"delete-a-topic",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Notifications",
|
||||
items: [
|
||||
"channel-notifications",
|
||||
"topic-notifications",
|
||||
"follow-a-topic",
|
||||
"dm-mention-alert-notifications",
|
||||
"mute-a-channel",
|
||||
"mute-a-topic",
|
||||
"mute-a-user",
|
||||
"email-notifications",
|
||||
"desktop-notifications",
|
||||
"mobile-notifications",
|
||||
"do-not-disturb",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Apps",
|
||||
items: [
|
||||
{
|
||||
label: "Download apps for every platform",
|
||||
link: "https://zulip.com/apps/",
|
||||
},
|
||||
"mobile-app-install-guide",
|
||||
"desktop-app-install-guide",
|
||||
"supported-browsers",
|
||||
"configure-how-links-open",
|
||||
"connect-through-a-proxy",
|
||||
"custom-certificates",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Zulip administration",
|
||||
link: "#",
|
||||
attrs: {
|
||||
class: "non-clickable-sidebar-heading",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Organization profile",
|
||||
items: [
|
||||
"organization-type",
|
||||
"communities-directory",
|
||||
"linking-to-zulip",
|
||||
"change-organization-url",
|
||||
"deactivate-your-organization",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Import an organization",
|
||||
items: [
|
||||
"import-from-mattermost",
|
||||
"import-from-slack",
|
||||
"import-from-rocketchat",
|
||||
"export-your-organization",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Account creation and authentication",
|
||||
items: [
|
||||
"configure-default-new-user-settings",
|
||||
"custom-profile-fields",
|
||||
"invite-new-users",
|
||||
"restrict-account-creation",
|
||||
"configure-authentication-methods",
|
||||
"saml-authentication",
|
||||
"scim",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "User management",
|
||||
items: [
|
||||
"manage-a-user",
|
||||
"deactivate-or-reactivate-a-user",
|
||||
"change-a-users-name",
|
||||
"manage-user-channel-subscriptions",
|
||||
"manage-user-group-membership",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Channel management",
|
||||
items: [
|
||||
"create-a-channel",
|
||||
{
|
||||
label: "Private channels",
|
||||
link: "/channel-permissions#private-channels",
|
||||
},
|
||||
{
|
||||
label: "Public channels",
|
||||
link: "/channel-permissions#public-channels",
|
||||
},
|
||||
"public-access-option",
|
||||
"general-chat-channels",
|
||||
"manage-channel-folders",
|
||||
"channel-permissions",
|
||||
"channel-posting-policy",
|
||||
"configure-who-can-administer-a-channel",
|
||||
"configure-who-can-create-channels",
|
||||
"configure-who-can-subscribe",
|
||||
"configure-who-can-invite-to-channels",
|
||||
"configure-who-can-unsubscribe-others",
|
||||
"subscribe-users-to-a-channel",
|
||||
"unsubscribe-users-from-a-channel",
|
||||
"set-default-channels-for-new-users",
|
||||
"rename-a-channel",
|
||||
"change-the-channel-description",
|
||||
"pin-information",
|
||||
"change-the-privacy-of-a-channel",
|
||||
"archive-a-channel",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Permissions management",
|
||||
items: [
|
||||
"manage-permissions",
|
||||
"manage-user-groups",
|
||||
"deactivate-a-user-group",
|
||||
"user-roles",
|
||||
"guest-users",
|
||||
"restrict-direct-messages",
|
||||
"restrict-wildcard-mentions",
|
||||
"restrict-message-editing-and-deletion",
|
||||
"restrict-message-edit-history-access",
|
||||
"restrict-moving-messages",
|
||||
"restrict-resolving-topics",
|
||||
"restrict-name-and-email-changes",
|
||||
"restrict-profile-picture-changes",
|
||||
"restrict-permissions-of-new-members",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Organization settings",
|
||||
items: [
|
||||
"configure-organization-language",
|
||||
"custom-emoji",
|
||||
"configure-call-provider",
|
||||
"add-a-custom-linkifier",
|
||||
"require-topics",
|
||||
"image-video-and-website-previews",
|
||||
"hide-message-content-in-emails",
|
||||
"message-retention-policy",
|
||||
"digest-emails",
|
||||
"disable-welcome-emails",
|
||||
"configure-automated-notices",
|
||||
"configure-multi-language-search",
|
||||
"analytics",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Bots & integrations",
|
||||
items: [
|
||||
"bots-overview",
|
||||
"integrations-overview",
|
||||
"add-a-bot-or-integration",
|
||||
"generate-integration-url",
|
||||
"manage-a-bot",
|
||||
"deactivate-or-reactivate-a-bot",
|
||||
"request-an-integration",
|
||||
"restrict-bot-creation",
|
||||
"view-your-bots",
|
||||
"view-all-bots-in-your-organization",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Support",
|
||||
items: [
|
||||
"view-zulip-version",
|
||||
"zulip-cloud-billing",
|
||||
"self-hosted-billing",
|
||||
"gdpr-compliance",
|
||||
"move-to-zulip-cloud",
|
||||
"support-zulip-project",
|
||||
"linking-to-zulip-website",
|
||||
"contact-support",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "◀ Back to Zulip",
|
||||
link: "../",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
54
starlight_help/package.json
Normal file
54
starlight_help/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "starlight_help",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"format": "remark --ext mdx --frail --output --quiet --use remark-mdx -- .",
|
||||
"format-silent": "remark --ext mdx --output --silent --use remark-mdx -- ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.3",
|
||||
"@astrojs/starlight": "^0.35.1",
|
||||
"@iconify-json/fa": "^1.2.1",
|
||||
"@types/hast": "^3.0.4",
|
||||
"astro": "^5.1.2",
|
||||
"hast-util-from-html": "^2.0.3",
|
||||
"hast-util-to-html": "^9.0.5",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"remark": "^15.0.1",
|
||||
"remark-mdx": "^3.1.0",
|
||||
"sharp": "^0.34.1",
|
||||
"typescript": "^5.4.5",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mdast": "^4.0.4",
|
||||
"remark-cli": "^12.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-lint-fenced-code-flag": "^4.2.0",
|
||||
"remark-lint-file-extension": "^3.0.1",
|
||||
"remark-lint-final-definition": "^4.0.2",
|
||||
"remark-lint-heading-increment": "^4.0.1",
|
||||
"remark-lint-list-item-indent": "^4.0.1",
|
||||
"remark-lint-list-item-spacing": "^5.0.1",
|
||||
"remark-lint-maximum-heading-length": "^4.1.1",
|
||||
"remark-lint-maximum-line-length": "^4.1.1",
|
||||
"remark-lint-no-duplicate-definitions": "^4.0.1",
|
||||
"remark-lint-no-duplicate-headings": "^4.0.1",
|
||||
"remark-lint-no-file-name-irregular-characters": "^3.0.1",
|
||||
"remark-lint-no-file-name-mixed-case": "^3.0.1",
|
||||
"remark-lint-no-unused-definitions": "^4.0.2",
|
||||
"remark-lint-unordered-list-marker-style": "^4.0.1",
|
||||
"remark-preset-lint-markdown-style-guide": "^6.0.1",
|
||||
"remark-preset-lint-recommended": "^7.0.1",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"unified": "^11.0.5"
|
||||
}
|
||||
}
|
61
starlight_help/src/.remarkrc.js
Normal file
61
starlight_help/src/.remarkrc.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// We are using remarkLintRulesLintRecommended and
|
||||
// remarkPresentLintMarkdownStyleGuide as our starting set of rules.
|
||||
// None of the rules were giving an error on the starting set, but some
|
||||
// rules were giving lots of warnings on the generated mdx. They are
|
||||
// set to false in this file, we can add them back later as and when
|
||||
// required.
|
||||
|
||||
/**
|
||||
* @import {Preset} from 'unified'
|
||||
*/
|
||||
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkLintFencedCodeFlag from "remark-lint-fenced-code-flag";
|
||||
import remarkLintFileExtension from "remark-lint-file-extension";
|
||||
import remarkLintFinalDefinition from "remark-lint-final-definition";
|
||||
import remarkLintHeadingIncrement from "remark-lint-heading-increment";
|
||||
import remarkLintListItemIndent from "remark-lint-list-item-indent";
|
||||
import remarkLintListItemSpacing from "remark-lint-list-item-spacing";
|
||||
import remarkLintMaximumHeadingLength from "remark-lint-maximum-heading-length";
|
||||
import remarkLintMaximumLineLength from "remark-lint-maximum-line-length";
|
||||
import remarkLintNoDuplicateDefinitions from "remark-lint-no-duplicate-definitions";
|
||||
import remarkLintNoDuplicateHeadings from "remark-lint-no-duplicate-headings";
|
||||
import remarkLintNoFileNameIrregularCharacters from "remark-lint-no-file-name-irregular-characters";
|
||||
import remarkLintNoFileNameMixedCase from "remark-lint-no-file-name-mixed-case";
|
||||
import remarkLintNoUnusedDefinitions from "remark-lint-no-unused-definitions";
|
||||
import remarkLintUnorderedListMarkerStyle from "remark-lint-unordered-list-marker-style";
|
||||
import remarkPresentLintMarkdownStyleGuide from "remark-preset-lint-markdown-style-guide";
|
||||
import remarkLintRulesLintRecommended from "remark-preset-lint-recommended";
|
||||
import remarkStringify from "remark-stringify";
|
||||
|
||||
const remarkLintRules = {
|
||||
plugins: [
|
||||
remarkLintRulesLintRecommended,
|
||||
remarkPresentLintMarkdownStyleGuide,
|
||||
[remarkLintFinalDefinition, false],
|
||||
[remarkLintListItemSpacing, false],
|
||||
[remarkLintFileExtension, ["mdx"]],
|
||||
[remarkLintNoUnusedDefinitions, false],
|
||||
[remarkLintMaximumLineLength, false],
|
||||
[remarkLintListItemIndent, false],
|
||||
[remarkLintFencedCodeFlag, false],
|
||||
[remarkLintNoFileNameIrregularCharacters, false],
|
||||
[remarkLintNoFileNameMixedCase, false],
|
||||
[remarkLintMaximumHeadingLength, false],
|
||||
[remarkLintNoDuplicateHeadings, false],
|
||||
[remarkLintHeadingIncrement, false],
|
||||
[remarkLintNoDuplicateDefinitions, false],
|
||||
[remarkLintUnorderedListMarkerStyle, "*"],
|
||||
],
|
||||
};
|
||||
|
||||
/** @type {Preset} */
|
||||
const config = {
|
||||
plugins: [
|
||||
[remarkFrontmatter, ["yaml"]],
|
||||
remarkLintRules,
|
||||
[remarkStringify, {incrementListMarker: false}],
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
36
starlight_help/src/components/EmoticonTranslations.astro
Normal file
36
starlight_help/src/components/EmoticonTranslations.astro
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import EmojiCodes from "../../../static/generated/emoji/emoji_codes.json";
|
||||
|
||||
const nameToCodePoint: Record<string, string> = EmojiCodes.name_to_codepoint;
|
||||
const rowHTML = (emoticon: string, codepoint: string, name: string) => `
|
||||
<tr>
|
||||
<td><code>${emoticon}</code></td>
|
||||
<td>
|
||||
<img
|
||||
src="/static/generated/emoji/images-google-64/${codepoint}.png"
|
||||
alt="${name}"
|
||||
class="emoji-big">
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
let body = "";
|
||||
const emoticonConversions: Record<string, string> =
|
||||
EmojiCodes.emoticon_conversions;
|
||||
for (const name of Object.keys(emoticonConversions)) {
|
||||
const emoticon: string = emoticonConversions[name]!;
|
||||
body += rowHTML(name, nameToCodePoint[emoticon.slice(1, -1)]!, emoticon);
|
||||
}
|
||||
---
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Emoticon</th>
|
||||
<th>Emoji</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Fragment set:html={body} />
|
||||
</tbody>
|
||||
</table>
|
43
starlight_help/src/components/FlattenedSteps.astro
Normal file
43
starlight_help/src/components/FlattenedSteps.astro
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {Steps} from "@astrojs/starlight/components";
|
||||
import {fromHtml} from "hast-util-from-html";
|
||||
import {toHtml} from "hast-util-to-html";
|
||||
|
||||
const tree = fromHtml(await Astro.slots.render("default"), {fragment: true});
|
||||
|
||||
const tree_with_removed_newlines = {
|
||||
type: "root",
|
||||
children: tree.children.filter((child) => {
|
||||
if (child.type === "text" && child.value === "\n") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
const first_element = tree_with_removed_newlines.children[0];
|
||||
assert.ok(
|
||||
first_element?.type === "element" &&
|
||||
["ol", "ul"].includes(first_element.tagName),
|
||||
);
|
||||
const flattened = {
|
||||
...first_element,
|
||||
children: tree_with_removed_newlines.children.flatMap((other) => {
|
||||
if (other.type === "comment") {
|
||||
return [];
|
||||
}
|
||||
assert.ok(other.type === "element");
|
||||
// Flatten only in case of matching tagName, for the rest, we
|
||||
// return the elements without flattening since asides, code
|
||||
// blocks and other elements can be part of a single list item
|
||||
// and we do not want to flatten them.
|
||||
if (other.tagName === first_element.tagName) {
|
||||
return other.children;
|
||||
}
|
||||
return [other];
|
||||
}),
|
||||
};
|
||||
---
|
||||
|
||||
<Steps><Fragment set:html={toHtml(flattened)} /></Steps>
|
22
starlight_help/src/components/Footer.astro
Normal file
22
starlight_help/src/components/Footer.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import {CORPORATE_ENABLED, SUPPORT_EMAIL} from "astro:env/client";
|
||||
|
||||
let footer_html = `<p>Don't see an answer to your question? <a href="mailto:{{ ${SUPPORT_EMAIL} }}">Contact this Zulip server's administrators</a> for support.</p>`;
|
||||
if (CORPORATE_ENABLED) {
|
||||
footer_html = `<p>Your feedback helps us make Zulip better for everyone! Please <a href="/starlight_help/contact-support">contact us</a> with questions, suggestions, and feature requests.</p>`;
|
||||
}
|
||||
---
|
||||
|
||||
<footer class="sl-flex">
|
||||
<hr />
|
||||
<Fragment set:html={footer_html} />
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
@layer starlight.core {
|
||||
footer {
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
6
starlight_help/src/components/Head.astro
Normal file
6
starlight_help/src/components/Head.astro
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
import Default from "@astrojs/starlight/components/Head.astro";
|
||||
---
|
||||
|
||||
<script src="../scripts/client/adjust_mac_kbd_tags.ts"></script>
|
||||
<Default><slot /></Default>
|
50
starlight_help/src/components/KeyboardTip.astro
Normal file
50
starlight_help/src/components/KeyboardTip.astro
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import type {Element} from "hast";
|
||||
import {fromHtml} from "hast-util-from-html";
|
||||
import {toHtml} from "hast-util-to-html";
|
||||
|
||||
import keyboard_svg from "../../../web/shared/icons/keyboard.svg?raw";
|
||||
|
||||
const keyboard_icon_fragment = fromHtml(keyboard_svg, {fragment: true});
|
||||
const keyboard_icon_first_child = keyboard_icon_fragment.children[0]!;
|
||||
assert.ok(
|
||||
keyboard_icon_first_child.type === "element" &&
|
||||
keyboard_icon_first_child.tagName === "svg",
|
||||
);
|
||||
keyboard_icon_first_child.properties.class = "zulip-unplugin-icon";
|
||||
|
||||
const prefix_element_list = [
|
||||
keyboard_icon_first_child,
|
||||
{
|
||||
type: "element",
|
||||
tagName: "strong",
|
||||
properties: {},
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
// Whitespace before the text to ensure space between
|
||||
// this text and the preceding icon.
|
||||
value: " Keyboard tip: ",
|
||||
},
|
||||
],
|
||||
} as Element,
|
||||
];
|
||||
|
||||
const tree = fromHtml(await Astro.slots.render("default"), {fragment: true});
|
||||
const first_element = tree.children[0];
|
||||
assert.ok(first_element?.type === "element");
|
||||
|
||||
first_element.children = [...prefix_element_list, ...first_element.children];
|
||||
tree.children[0] = first_element;
|
||||
---
|
||||
|
||||
<aside
|
||||
aria-label="Keyboard tip"
|
||||
class=`starlight-aside starlight-aside--tip keyboard-tip`
|
||||
>
|
||||
<div class="starlight-aside__content">
|
||||
<Fragment set:html={toHtml(tree)} />
|
||||
</div>
|
||||
</aside>
|
416
starlight_help/src/components/NavigationSteps.astro
Normal file
416
starlight_help/src/components/NavigationSteps.astro
Normal file
@@ -0,0 +1,416 @@
|
||||
---
|
||||
import assert from "node:assert";
|
||||
|
||||
import {CORPORATE_ENABLED, SHOW_RELATIVE_LINKS} from "astro:env/client";
|
||||
|
||||
/* eslint-disable import/extensions */
|
||||
import RawBarChartIcon from "~icons/zulip-icon/bar-chart?raw";
|
||||
import RawBuildingIcon from "~icons/zulip-icon/building?raw";
|
||||
import RawCreditCardIcon from "~icons/zulip-icon/credit-card?raw";
|
||||
import RawEditIcon from "~icons/zulip-icon/edit?raw";
|
||||
import RawGearIcon from "~icons/zulip-icon/gear?raw";
|
||||
import RawGitPullRequestIcon from "~icons/zulip-icon/git-pull-request?raw";
|
||||
import RawHashIcon from "~icons/zulip-icon/hash?raw";
|
||||
import RawHelpIcon from "~icons/zulip-icon/help?raw";
|
||||
import RawInfoIcon from "~icons/zulip-icon/info?raw";
|
||||
import RawKeyboardIcon from "~icons/zulip-icon/keyboard?raw";
|
||||
import RawManageSearchIcon from "~icons/zulip-icon/manage-search?raw";
|
||||
import RawRocketIcon from "~icons/zulip-icon/rocket?raw";
|
||||
import RawToolIcon from "~icons/zulip-icon/tool?raw";
|
||||
import RawUserGroupCogIcon from "~icons/zulip-icon/user-group-cog?raw";
|
||||
/* eslint-enable import/extensions */
|
||||
|
||||
const PERSONAL_SETTINGS_TYPE = "Personal settings";
|
||||
const ORGANIZATION_SETTINGS_TYPE = "Organization settings";
|
||||
const SHOW_BILLING_HELP_LINKS = CORPORATE_ENABLED;
|
||||
|
||||
// This list has been transformed one-off from `help_settings_links.py`, we
|
||||
// have added a comment in that file to update this list in case of any
|
||||
// changes.
|
||||
const setting_link_mapping: Record<
|
||||
string,
|
||||
{
|
||||
setting_type: string;
|
||||
setting_name: string;
|
||||
setting_link: string;
|
||||
}
|
||||
> = {
|
||||
// a mapping from the setting target that is the same as the final URL
|
||||
// breadcrumb to that setting to the name of its setting type, the setting
|
||||
// name as it appears in the user interface, and a relative link that can
|
||||
// be used to get to that setting
|
||||
profile: {
|
||||
setting_type: PERSONAL_SETTINGS_TYPE,
|
||||
setting_name: "Profile",
|
||||
setting_link: "/#settings/profile",
|
||||
},
|
||||
"account-and-privacy": {
|
||||
setting_type: PERSONAL_SETTINGS_TYPE,
|
||||
setting_name: "Account & privacy",
|
||||
setting_link: "/#settings/account-and-privacy",
|
||||
},
|
||||
preferences: {
|
||||
setting_type: PERSONAL_SETTINGS_TYPE,
|
||||
setting_name: "Preferences",
|
||||
setting_link: "/#settings/preferences",
|
||||
},
|
||||
notifications: {
|
||||
setting_type: PERSONAL_SETTINGS_TYPE,
|
||||
setting_name: "Notifications",
|
||||
setting_link: "/#settings/notifications",
|
||||
},
|
||||
"your-bots": {
|
||||
setting_type: PERSONAL_SETTINGS_TYPE,
|
||||
setting_name: "Bots",
|
||||
setting_link: "/#settings/your-bots",
|
||||
},
|
||||
"alert-words": {
|
||||
setting_type: PERSONAL_SETTINGS_TYPE,
|
||||
setting_name: "Alert words",
|
||||
setting_link: "/#settings/alert-words",
|
||||
},
|
||||
"uploaded-files": {
|
||||
setting_type: PERSONAL_SETTINGS_TYPE,
|
||||
setting_name: "Uploaded files",
|
||||
setting_link: "/#settings/uploaded-files",
|
||||
},
|
||||
topics: {
|
||||
setting_type: PERSONAL_SETTINGS_TYPE,
|
||||
setting_name: "Topics",
|
||||
setting_link: "/#settings/topics",
|
||||
},
|
||||
"muted-users": {
|
||||
setting_type: PERSONAL_SETTINGS_TYPE,
|
||||
setting_name: "Muted users",
|
||||
setting_link: "/#settings/muted-users",
|
||||
},
|
||||
"organization-profile": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Organization profile",
|
||||
setting_link: "/#organization/organization-profile",
|
||||
},
|
||||
"organization-settings": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Organization settings",
|
||||
setting_link: "/#organization/organization-settings",
|
||||
},
|
||||
"organization-permissions": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Organization permissions",
|
||||
setting_link: "/#organization/organization-permissions",
|
||||
},
|
||||
"default-user-settings": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Default user settings",
|
||||
setting_link: "/#organization/organization-level-user-defaults",
|
||||
},
|
||||
"emoji-settings": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Custom emoji",
|
||||
setting_link: "/#organization/emoji-settings",
|
||||
},
|
||||
"auth-methods": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Authentication methods",
|
||||
setting_link: "/#organization/auth-methods",
|
||||
},
|
||||
users: {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Users",
|
||||
setting_link: "/#organization/users/active",
|
||||
},
|
||||
deactivated: {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Users",
|
||||
setting_link: "/#organization/users/deactivated",
|
||||
},
|
||||
invitations: {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Users",
|
||||
setting_link: "/#organization/users/invitations",
|
||||
},
|
||||
"bot-list-admin": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Bots",
|
||||
setting_link: "/#organization/bot-list-admin",
|
||||
},
|
||||
"default-channels-list": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Default channels",
|
||||
setting_link: "/#organization/default-channels-list",
|
||||
},
|
||||
"linkifier-settings": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Linkifiers",
|
||||
setting_link: "/#organization/linkifier-settings",
|
||||
},
|
||||
"playground-settings": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Code playgrounds",
|
||||
setting_link: "/#organization/playground-settings",
|
||||
},
|
||||
"profile-field-settings": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Custom profile fields",
|
||||
setting_link: "/#organization/profile-field-settings",
|
||||
},
|
||||
"channel-folder-settings": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Channel folders",
|
||||
setting_link: "/#organization/channel-folders",
|
||||
},
|
||||
"data-exports-admin": {
|
||||
setting_type: ORGANIZATION_SETTINGS_TYPE,
|
||||
setting_name: "Data exports",
|
||||
setting_link: "/#organization/data-exports-admin",
|
||||
},
|
||||
};
|
||||
|
||||
type RelativeLinkInfo = {
|
||||
label: string;
|
||||
relative_link: string;
|
||||
icon?: string | undefined;
|
||||
};
|
||||
|
||||
const default_template_for_relative_links = `
|
||||
<ol>
|
||||
<li>Click on the <strong>gear</strong> (${RawGearIcon}) icon in the upper right corner of the web or desktop app.</li>
|
||||
<li class="navigation-step-relative-type">Select {item}.</li>
|
||||
</ol>
|
||||
`;
|
||||
|
||||
const relative_link_mapping: Record<
|
||||
string,
|
||||
{
|
||||
data: Record<string, RelativeLinkInfo>;
|
||||
template: string;
|
||||
is_link_relative: () => boolean;
|
||||
}
|
||||
> = {
|
||||
gear: {
|
||||
data: {
|
||||
"channel-settings": {
|
||||
label: "Channel settings",
|
||||
relative_link: "/#channels/subscribed",
|
||||
icon: `${RawHashIcon}`,
|
||||
},
|
||||
settings: {
|
||||
label: "Personal Settings",
|
||||
relative_link: "/#settings/profile",
|
||||
icon: `${RawToolIcon}`,
|
||||
},
|
||||
"organization-settings": {
|
||||
label: "Organization settings",
|
||||
relative_link: "/#organization/organization-profile",
|
||||
icon: `${RawBuildingIcon}`,
|
||||
},
|
||||
"group-settings": {
|
||||
label: "Group settings",
|
||||
relative_link: "/#groups/your",
|
||||
icon: `${RawUserGroupCogIcon}`,
|
||||
},
|
||||
stats: {
|
||||
label: "Usage statistics",
|
||||
relative_link: "/stats",
|
||||
icon: `${RawBarChartIcon}`,
|
||||
},
|
||||
integrations: {
|
||||
label: "Integrations",
|
||||
relative_link: "/integrations/",
|
||||
icon: `${RawGitPullRequestIcon}`,
|
||||
},
|
||||
"about-zulip": {
|
||||
label: "About Zulip",
|
||||
relative_link: "/#about-zulip",
|
||||
},
|
||||
},
|
||||
template: default_template_for_relative_links,
|
||||
is_link_relative: () => SHOW_RELATIVE_LINKS,
|
||||
},
|
||||
"gear-billing": {
|
||||
data: {
|
||||
plans: {
|
||||
label: "Plans and pricing",
|
||||
relative_link: "/plans/",
|
||||
icon: `${RawRocketIcon}`,
|
||||
},
|
||||
billing: {
|
||||
label: "Billing",
|
||||
relative_link: "/billing/",
|
||||
icon: `${RawCreditCardIcon}`,
|
||||
},
|
||||
},
|
||||
template: default_template_for_relative_links,
|
||||
is_link_relative: () => SHOW_RELATIVE_LINKS && SHOW_BILLING_HELP_LINKS,
|
||||
},
|
||||
help: {
|
||||
data: {
|
||||
"keyboard-shortcuts": {
|
||||
label: "Keyboard shortcuts",
|
||||
relative_link: "/#keyboard-shortcuts",
|
||||
icon: `${RawKeyboardIcon}`,
|
||||
},
|
||||
"message-formatting": {
|
||||
label: "Message formatting",
|
||||
relative_link: "/#message-formatting",
|
||||
icon: `${RawEditIcon}`,
|
||||
},
|
||||
"search-filters": {
|
||||
label: "Search filters",
|
||||
relative_link: "/#search-operators",
|
||||
icon: `${RawManageSearchIcon}`,
|
||||
},
|
||||
"about-zulip": {
|
||||
label: "About Zulip",
|
||||
relative_link: "/#about-zulip",
|
||||
icon: `${RawInfoIcon}`,
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<ol>
|
||||
<li>Click on the <strong>Help menu</strong> (${RawHelpIcon}) icon in the upper right corner of the app.</li>
|
||||
<li class="navigation-step-relative-type">Select {item}.</li>
|
||||
</ol>
|
||||
`,
|
||||
is_link_relative: () => SHOW_RELATIVE_LINKS,
|
||||
},
|
||||
channel: {
|
||||
data: {
|
||||
all: {
|
||||
label: "All",
|
||||
relative_link: "/#channels/all",
|
||||
},
|
||||
"not-subscribed": {
|
||||
label: "Not subscribed",
|
||||
relative_link: "/#channels/notsubscribed",
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<ol>
|
||||
<li>Click on the <strong>gear</strong> (${RawGearIcon}) icon in the upper right corner of the web or desktop app.</li>
|
||||
<li>Select ${RawHashIcon} <strong>Channel settings</strong>.</li>
|
||||
<li>Click {item} in the upper left.</li>
|
||||
</ol>
|
||||
`,
|
||||
is_link_relative: () => SHOW_RELATIVE_LINKS,
|
||||
},
|
||||
group: {
|
||||
data: {
|
||||
all: {
|
||||
label: "All groups",
|
||||
relative_link: "/#groups/all",
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<ol>
|
||||
<li>Click on the <strong>gear</strong> (${RawGearIcon}) icon in the upper right corner of the web or desktop app.</li>
|
||||
<li>Select ${RawUserGroupCogIcon} <strong>Group settings</strong>.</li>
|
||||
<li>Click {item} in the upper left.</li>
|
||||
</ol>
|
||||
`,
|
||||
is_link_relative: () => SHOW_RELATIVE_LINKS,
|
||||
},
|
||||
};
|
||||
|
||||
const getSettingsMarkdown = (
|
||||
setting_type: string,
|
||||
setting_type_icon: string,
|
||||
setting_name: string,
|
||||
) => `
|
||||
<ol>
|
||||
<li>
|
||||
Click on the <b>gear</b> (${RawGearIcon}) icon in the upper
|
||||
right corner of the web or desktop app.
|
||||
</li>
|
||||
<li>
|
||||
Select <b>${setting_type_icon} ${setting_type}</b>.
|
||||
</li>
|
||||
<li>
|
||||
On the left, click <b>${setting_name}</b>.
|
||||
</li>
|
||||
</ol>
|
||||
`;
|
||||
|
||||
const getSettingsHTML = (
|
||||
setting_key: string,
|
||||
SHOW_RELATIVE_LINKS: boolean,
|
||||
): string => {
|
||||
const {setting_type, setting_name, setting_link} =
|
||||
setting_link_mapping[setting_key]!;
|
||||
|
||||
if (!SHOW_RELATIVE_LINKS) {
|
||||
const setting_type_icon =
|
||||
setting_type === ORGANIZATION_SETTINGS_TYPE
|
||||
? `${RawBuildingIcon}`.trim()
|
||||
: `${RawToolIcon}`.trim();
|
||||
return getSettingsMarkdown(
|
||||
setting_type,
|
||||
setting_type_icon,
|
||||
setting_name,
|
||||
);
|
||||
}
|
||||
|
||||
const relativeLink = `<a href="${setting_link}">${setting_name}</a>`;
|
||||
|
||||
// The "Bots" label appears in both Personal and
|
||||
// Organization settings in the user interface, so we need special
|
||||
// text for this setting.
|
||||
// As for the the case of "Users", it refers to the Users tab in
|
||||
// organization settings. Since the users tab has multiple sub tabs
|
||||
// like active, deactivated etc., we need a way to point to them.
|
||||
const label =
|
||||
setting_name === "Bots" || setting_name === "Users"
|
||||
? `Navigate to the ${relativeLink} tab of the <b>${setting_type}</b> menu.`
|
||||
: `Go to ${relativeLink}.`;
|
||||
|
||||
return `<ol>
|
||||
<li>${label}</li>
|
||||
</ol>`;
|
||||
};
|
||||
|
||||
const RELATIVE_NAVIGATION_HANDLERS_BY_TYPE: Record<
|
||||
string,
|
||||
(key: string) => string
|
||||
> = {};
|
||||
|
||||
for (const type of Object.keys(relative_link_mapping)) {
|
||||
const {data, template, is_link_relative} = relative_link_mapping[type]!;
|
||||
|
||||
RELATIVE_NAVIGATION_HANDLERS_BY_TYPE[type] = (key: string) => {
|
||||
const {label, relative_link, icon} = data[key]!;
|
||||
let formattedLabel = label;
|
||||
if (icon !== undefined) {
|
||||
const trimmedIcon = `${icon}`.trim();
|
||||
formattedLabel = trimmedIcon + label;
|
||||
}
|
||||
const formattedItem = is_link_relative()
|
||||
? `<a href="${relative_link}">${formattedLabel}</a>`
|
||||
: `<strong>${formattedLabel}</strong>`;
|
||||
return template.replace("{item}", formattedItem);
|
||||
};
|
||||
}
|
||||
|
||||
const {target} = Astro.props;
|
||||
const navigation_link_type = target.split("/")[0];
|
||||
|
||||
if (
|
||||
navigation_link_type !== "settings" &&
|
||||
navigation_link_type !== "relative"
|
||||
) {
|
||||
throw new Error(
|
||||
"Invalid navigation link type. Only `settings` or `relative` is allowed.",
|
||||
);
|
||||
}
|
||||
|
||||
let resultHTML: string | undefined;
|
||||
if (navigation_link_type === "settings") {
|
||||
resultHTML = getSettingsHTML(target.split("/")[1], SHOW_RELATIVE_LINKS);
|
||||
} else {
|
||||
const link_type = target.split("/")[1];
|
||||
const key = target.split("/")[2];
|
||||
resultHTML = RELATIVE_NAVIGATION_HANDLERS_BY_TYPE[link_type]!(key);
|
||||
}
|
||||
assert.ok(resultHTML !== undefined);
|
||||
---
|
||||
|
||||
<Fragment set:html={resultHTML} />
|
11
starlight_help/src/components/ZulipNote.astro
Normal file
11
starlight_help/src/components/ZulipNote.astro
Normal file
@@ -0,0 +1,11 @@
|
||||
<!--
|
||||
We wanted to have our note without a header that is the ⓘ followed by a title.
|
||||
We can make the title disappear by making the title a single space: " ".
|
||||
The default aside component provided by starlight will always have an icon however.
|
||||
That is why we needed this custom components.
|
||||
-->
|
||||
<aside aria-label="Note" class={`starlight-aside starlight-aside--note`}>
|
||||
<div class="starlight-aside__content">
|
||||
<slot />
|
||||
</div>
|
||||
</aside>
|
62
starlight_help/src/components/ZulipTip.astro
Normal file
62
starlight_help/src/components/ZulipTip.astro
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import type {Element} from "hast";
|
||||
import {fromHtml} from "hast-util-from-html";
|
||||
import {toHtml} from "hast-util-to-html";
|
||||
|
||||
import lightbulb_svg from "../../../web/shared/icons/lightbulb.svg?raw";
|
||||
|
||||
const lightbulb_icon_fragment = fromHtml(lightbulb_svg, {fragment: true});
|
||||
const lightbulb_icon_first_child = lightbulb_icon_fragment.children[0]!;
|
||||
assert.ok(
|
||||
lightbulb_icon_first_child.type === "element" &&
|
||||
lightbulb_icon_first_child.tagName === "svg",
|
||||
);
|
||||
lightbulb_icon_first_child.properties.class =
|
||||
"zulip-unplugin-icon aside-icon-lightbulb";
|
||||
|
||||
let prefix_element_list = [
|
||||
lightbulb_icon_first_child,
|
||||
{
|
||||
type: "element",
|
||||
tagName: "strong",
|
||||
properties: {},
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
// Whitespace before the text to ensure space between
|
||||
// this text and the preceding icon.
|
||||
value: " Tip: ",
|
||||
},
|
||||
],
|
||||
} as Element,
|
||||
];
|
||||
|
||||
const tree = fromHtml(await Astro.slots.render("default"), {fragment: true});
|
||||
const first_element = tree.children[0];
|
||||
assert.ok(first_element?.type === "element");
|
||||
|
||||
// This is currently happening only in one case, for _ImportSelfHostedServerTips.mdx
|
||||
// where the tip contains an unordered list. Just placing the element as is without
|
||||
// a paragraph does not look good in that case.
|
||||
if (first_element.tagName !== "p") {
|
||||
prefix_element_list = [
|
||||
{
|
||||
type: "element",
|
||||
tagName: "p",
|
||||
properties: {},
|
||||
children: [...prefix_element_list],
|
||||
} as Element,
|
||||
];
|
||||
}
|
||||
|
||||
first_element.children = [...prefix_element_list, ...first_element.children];
|
||||
tree.children[0] = first_element;
|
||||
---
|
||||
|
||||
<aside aria-label="Tip" class=`starlight-aside starlight-aside--tip`>
|
||||
<div class="starlight-aside__content">
|
||||
<Fragment set:html={toHtml(tree)} />
|
||||
</div>
|
||||
</aside>
|
7
starlight_help/src/content.config.ts
Normal file
7
starlight_help/src/content.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {docsLoader} from "@astrojs/starlight/loaders";
|
||||
import {docsSchema} from "@astrojs/starlight/schema";
|
||||
import {defineCollection} from "astro:content";
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({loader: docsLoader(), schema: docsSchema()}),
|
||||
};
|
4
starlight_help/src/env.d.ts
vendored
Normal file
4
starlight_help/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
||||
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
36
starlight_help/src/route_data.ts
Normal file
36
starlight_help/src/route_data.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import assert from "node:assert";
|
||||
|
||||
import {defineRouteMiddleware} from "@astrojs/starlight/route-data";
|
||||
import type {Paragraph} from "mdast";
|
||||
import {toString} from "mdast-util-to-string";
|
||||
import {remark} from "remark";
|
||||
import remarkMdx from "remark-mdx";
|
||||
import {visit} from "unist-util-visit";
|
||||
|
||||
function extractFirstParagraph(content: string): string | undefined {
|
||||
const tree = remark().use(remarkMdx).parse(content);
|
||||
|
||||
let firstParagraph: string | undefined;
|
||||
|
||||
visit(tree, "paragraph", (node: Paragraph) => {
|
||||
if (!firstParagraph) {
|
||||
// We need to convert the node to string so that links, emphasis, etc.
|
||||
// are converted to plain text.
|
||||
firstParagraph = toString(node);
|
||||
firstParagraph = firstParagraph.replaceAll(/\s+/g, " ").trim();
|
||||
}
|
||||
});
|
||||
|
||||
return firstParagraph;
|
||||
}
|
||||
|
||||
export const onRequest = defineRouteMiddleware((context) => {
|
||||
assert.ok(typeof context.locals.starlightRoute.entry.body === "string");
|
||||
context.locals.starlightRoute.head.push({
|
||||
tag: "meta",
|
||||
attrs: {
|
||||
name: "description",
|
||||
content: extractFirstParagraph(context.locals.starlightRoute.entry.body),
|
||||
},
|
||||
});
|
||||
});
|
66
starlight_help/src/scripts/client/adjust_mac_kbd_tags.ts
Normal file
66
starlight_help/src/scripts/client/adjust_mac_kbd_tags.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// Any changes to this file should be followed by a check for changes
|
||||
// needed to make to adjust_mac_kbd_tags of web/src/common.ts.
|
||||
|
||||
const keys_map = new Map<string, string>([
|
||||
["Backspace", "Delete"],
|
||||
["Enter", "Return"],
|
||||
["Ctrl", "⌘"],
|
||||
["Alt", "⌥"],
|
||||
]);
|
||||
|
||||
function has_mac_keyboard(): boolean {
|
||||
"use strict";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
return /mac/i.test(navigator.platform);
|
||||
}
|
||||
|
||||
// We convert the <kbd> tags used for keyboard shortcuts to mac equivalent
|
||||
// key combinations, when we detect that the user is using a mac-style keyboard.
|
||||
function adjust_mac_kbd_tags(): void {
|
||||
"use strict";
|
||||
|
||||
if (!has_mac_keyboard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = document.querySelectorAll<HTMLElement>("kbd");
|
||||
|
||||
for (const element of elements) {
|
||||
let key_text: string = element.textContent ?? "";
|
||||
|
||||
// We use data-mac-key attribute to override the default key in case
|
||||
// of exceptions:
|
||||
// - There are 2 shortcuts (for navigating back and forth in browser
|
||||
// history) which need "⌘" instead of the expected mapping ("Opt")
|
||||
// for the "Alt" key, so we use this attribute to override "Opt"
|
||||
// with "⌘".
|
||||
// - The "Ctrl" + "[" shortcuts (which match the Vim keybinding behavior
|
||||
// of mapping to "Esc") need to display "Ctrl" for all users, so we
|
||||
// use this attribute to override "⌘" with "Ctrl".
|
||||
const replace_key: string | undefined = element.dataset.macKey ?? keys_map.get(key_text);
|
||||
if (replace_key !== undefined) {
|
||||
key_text = replace_key;
|
||||
}
|
||||
|
||||
element.textContent = key_text;
|
||||
|
||||
// In case of shortcuts, the Mac equivalent of which involves extra keys,
|
||||
// we use data-mac-following-key attribute to append the extra key to the
|
||||
// previous key. Currently, this is used to append Opt to Cmd for the Paste
|
||||
// as plain text shortcut.
|
||||
const following_key: string | undefined = element.dataset.macFollowingKey;
|
||||
if (following_key !== undefined) {
|
||||
const kbd_elem: HTMLElement = document.createElement("kbd");
|
||||
kbd_elem.textContent = following_key;
|
||||
element.after(kbd_elem);
|
||||
element.after(" + ");
|
||||
}
|
||||
|
||||
// In web/src/common.ts, we use zulip icon for ⌘ due to centering
|
||||
// problems, we don't have that problem in the new help center and
|
||||
// thus don't do that transformation here.
|
||||
}
|
||||
}
|
||||
|
||||
adjust_mac_kbd_tags();
|
243
starlight_help/src/styles/main.css
Normal file
243
starlight_help/src/styles/main.css
Normal file
@@ -0,0 +1,243 @@
|
||||
:root {
|
||||
/* Starlight base style headings are huge and distract from reading the text.
|
||||
This is OK for some sites, but we are sparing with text and really want to
|
||||
encourage users to read it. */
|
||||
--sl-text-h1: 2rem;
|
||||
--sl-text-h2: 1.4rem;
|
||||
--sl-text-h3: 1.2rem;
|
||||
--sl-text-h4: 1rem;
|
||||
--sl-text-h5: 1rem;
|
||||
|
||||
/* Changed from 1.2 to 1 */
|
||||
--sl-line-height-headings: 1;
|
||||
|
||||
/* Changed from 1.75 to make text easier to read. */
|
||||
--sl-line-height: 1.5;
|
||||
|
||||
/* User circles */
|
||||
/* stylelint-disable color-no-hex */
|
||||
--color-user-circle-active: light-dark(#43a35e, #4cdc75);
|
||||
--color-user-circle-idle: light-dark(#f5b266, #ae640a);
|
||||
--color-user-circle-offline: light-dark(#c1c6d7, #454854);
|
||||
--color-user-circle-deactivated: hsl(0deg 0% 50%);
|
||||
/* stylelint-enable color-no-hex */
|
||||
|
||||
/* NOTE: These colors are also used in zulip web app for banner
|
||||
colors. Do grep for these variables when changing them and
|
||||
confirm on CZO on whether the colors there need to change as
|
||||
well. */
|
||||
/* Banners - Neutral Variant */
|
||||
--color-text-neutral-banner: light-dark(
|
||||
hsl(229deg 12% 25%),
|
||||
hsl(231deg 11% 76%)
|
||||
);
|
||||
--color-border-neutral-banner: light-dark(
|
||||
color-mix(in oklch, hsl(240deg 2% 30%) 40%, transparent),
|
||||
color-mix(in oklch, hsl(240deg 7% 66%) 40%, transparent)
|
||||
);
|
||||
--color-background-neutral-banner: light-dark(
|
||||
hsl(240deg 7% 93%),
|
||||
hsl(240deg 7% 17%)
|
||||
);
|
||||
/* Banners - Brand Variant */
|
||||
--color-text-brand-banner: light-dark(
|
||||
hsl(264deg 95% 34%),
|
||||
hsl(244deg 96% 82%)
|
||||
);
|
||||
--color-border-brand-banner: light-dark(
|
||||
color-mix(in oklch, hsl(254deg 60% 50%) 40%, transparent),
|
||||
color-mix(in oklch, hsl(253deg 70% 89%) 40%, transparent)
|
||||
);
|
||||
--color-background-brand-banner: light-dark(
|
||||
hsl(254deg 42% 94%),
|
||||
hsl(253deg 49% 16%)
|
||||
);
|
||||
/* Banners - Info Variant */
|
||||
--color-text-info-banner: light-dark(
|
||||
hsl(241deg 95% 25%),
|
||||
hsl(221deg 93% 89%)
|
||||
);
|
||||
--color-border-info-banner: light-dark(
|
||||
color-mix(in oklch, hsl(204deg 49% 29%) 40%, transparent),
|
||||
color-mix(in oklch, hsl(205deg 58% 69%) 40%, transparent)
|
||||
);
|
||||
--color-background-info-banner: light-dark(
|
||||
hsl(204deg 58% 92%),
|
||||
hsl(204deg 100% 12%)
|
||||
);
|
||||
|
||||
/* Keyboard shortcuts */
|
||||
--color-keyboard-shortcuts: light-dark(
|
||||
hsl(225deg 57.09% 42.9%),
|
||||
hsl(225deg 100% 84%)
|
||||
);
|
||||
}
|
||||
|
||||
.non-clickable-sidebar-heading {
|
||||
font-size: 1.15rem;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Eliminate the border inserted between the title and the rest of
|
||||
the content. */
|
||||
.content-panel + .content-panel {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
/* Decrease padding for the content panel from 1.5rem to 1rem since
|
||||
the padding looked too big after removing the content panel border. */
|
||||
.content-panel {
|
||||
padding: 1rem var(--sl-content-pad-x);
|
||||
}
|
||||
|
||||
.zulip-unplugin-icon {
|
||||
/* Make sure the icon does not occupy it's own row. */
|
||||
display: inline;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
/* unplugin-icons sets height and width by itself.
|
||||
It was setting the height to 1024 and 960 for some
|
||||
icons. It is better to set the height explicitly. */
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
|
||||
/* Some css rules in starlight insert these margins to tags
|
||||
that fit certain criteria, e.g. if it's a first child of
|
||||
an li item and similar cases, and the icon disturbs the
|
||||
spacing of everything around it just because it was an
|
||||
svg tag. We set this explicitly to zero to avoid those
|
||||
issues. */
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
|
||||
/* We need to specify this for dark mode. */
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
.navigation-step-relative-type .zulip-unplugin-icon {
|
||||
/* There's no space between the icon and text for navigation
|
||||
step labels because of any text decoration rules when these
|
||||
steps have a relative link. So we add a right margin to the
|
||||
icon to add the white space without any text decoration. */
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.starlight-aside--tip {
|
||||
--sl-color-asides-text-accent: var(--color-text-brand-banner);
|
||||
--sl-color-asides-border: var(--color-border-brand-banner);
|
||||
background-color: var(--color-background-brand-banner);
|
||||
}
|
||||
|
||||
.starlight-aside--note {
|
||||
--sl-color-asides-text-accent: var(--color-text-neutral-banner);
|
||||
--sl-color-asides-border: var(--color-border-neutral-banner);
|
||||
background-color: var(--color-background-neutral-banner);
|
||||
}
|
||||
|
||||
.keyboard-tip {
|
||||
--sl-color-asides-text-accent: var(--color-text-info-banner);
|
||||
--sl-color-asides-border: var(--color-border-info-banner);
|
||||
background-color: var(--color-background-info-banner);
|
||||
}
|
||||
|
||||
.aside-icon-lightbulb {
|
||||
/* We need to make the fill transparent for the bulb to look hollow
|
||||
and the default vertical-align of text-bottom was not looking
|
||||
good beside the `Tip` text. */
|
||||
vertical-align: text-top;
|
||||
fill: transparent;
|
||||
stroke: currentcolor;
|
||||
/* In cases where content spanned across multiple lines, the
|
||||
icon and the content just below it did not look aligned. */
|
||||
margin-left: -3px;
|
||||
/* Using any of the default vertical-align did not give desired
|
||||
results, text-top + this margin looked the best. */
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.user-circle {
|
||||
font-size: 0.7em;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.user-circle-active {
|
||||
color: var(--color-user-circle-active);
|
||||
}
|
||||
|
||||
.user-circle-idle {
|
||||
color: var(--color-user-circle-idle);
|
||||
}
|
||||
|
||||
.user-circle-offline {
|
||||
color: var(--color-user-circle-offline);
|
||||
}
|
||||
|
||||
.user-circle-deactivated {
|
||||
color: var(--color-user-circle-deactivated);
|
||||
}
|
||||
|
||||
.sl-markdown-content {
|
||||
img {
|
||||
vertical-align: top;
|
||||
box-shadow: 0 0 4px hsl(0deg 0% 0% / 5%);
|
||||
border: 1px solid hsl(0deg 0% 87%);
|
||||
border-radius: 4px;
|
||||
margin-top: 0;
|
||||
|
||||
&.emoji-small {
|
||||
display: inline-block;
|
||||
width: 1.25em;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
&.emoji-big {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
&.help-center-icon {
|
||||
display: inline-block;
|
||||
width: 1.25em;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
}
|
||||
|
||||
li > ul,
|
||||
li > ol {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
& .sl-heading-wrapper:has(> :first-child:target) {
|
||||
/* Increase the highlighted space around the text... */
|
||||
/* We are trying to recreate `padding: 6px 0 6px 8px` below
|
||||
using box-shadow since we don't want padding to affect the
|
||||
layout. A spread of 6px will make sure of the 6px part of
|
||||
the padding, and -2px will ensure a padding of 8px is
|
||||
recreated on the left side. */
|
||||
box-shadow: -2px 0 0 6px var(--sl-color-accent-low);
|
||||
background-color: var(--sl-color-accent-low);
|
||||
}
|
||||
|
||||
& kbd {
|
||||
font-size: 1.2em;
|
||||
padding: 0.15em 0.4em 0.05em;
|
||||
border: 1px solid var(--color-keyboard-shortcuts);
|
||||
border-radius: 3px;
|
||||
color: var(--color-keyboard-shortcuts);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
font-size: 0.85em;
|
||||
}
|
35
starlight_help/tsconfig.json
Normal file
35
starlight_help/tsconfig.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
/* Type Checking */
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strict": true,
|
||||
|
||||
/* Modules */
|
||||
"module": "preserve",
|
||||
|
||||
/* Emit */
|
||||
"noEmit": true,
|
||||
|
||||
/* Interop Constraints */
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "esnext",
|
||||
|
||||
/* Projects */
|
||||
"composite": true,
|
||||
|
||||
"types": ["unplugin-icons/types/astro"],
|
||||
},
|
||||
"include": [".astro/types.d.ts", "**/*", "src/.remarkrc.js"],
|
||||
"exclude": ["dist"],
|
||||
}
|
Reference in New Issue
Block a user