@hraness/design-kit 0.31.1
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/ARTICLE_COPY.md +208 -0
- package/HERO_FIELDS.md +55 -0
- package/ICONS.md +56 -0
- package/LANTERN_MATERIAL.md +60 -0
- package/LICENSE +21 -0
- package/MARKETING_COPY.md +49 -0
- package/MARKETING_PRESET.md +108 -0
- package/PALETTES.md +90 -0
- package/PAPER_THEME.md +128 -0
- package/README.md +745 -0
- package/STATUS_PAGES.md +67 -0
- package/STYLE.md +245 -0
- package/WRITING.md +72 -0
- package/dist/browser/index.js +2345 -0
- package/dist/chunk-44djxk16.js +6872 -0
- package/dist/chunk-5gtx3pza.js +9 -0
- package/dist/chunk-77391vmq.js +454 -0
- package/dist/chunk-7b1586eb.js +1209 -0
- package/dist/chunk-8834fh4n.js +6 -0
- package/dist/chunk-9t8xyqte.js +870 -0
- package/dist/chunk-cejpzyfh.js +446 -0
- package/dist/chunk-eh71jz57.js +429 -0
- package/dist/chunk-evb02bc1.js +516 -0
- package/dist/chunk-h4k7yv6x.js +368 -0
- package/dist/chunk-he8eznb1.js +760 -0
- package/dist/chunk-mr1vdcjq.js +404 -0
- package/dist/chunk-wzvdn8ey.js +116 -0
- package/dist/fonts/nebula-sans/social-fonts.generated.js +53 -0
- package/dist/icons.js +48 -0
- package/dist/index.js +237 -0
- package/dist/launch.js +41 -0
- package/dist/mockups/client.js +422 -0
- package/dist/mockups/index.js +1850 -0
- package/dist/portfolio.js +1549 -0
- package/dist/provider-marks.js +19 -0
- package/dist/react/charts.js +11 -0
- package/dist/react/hero-backdrop.js +8 -0
- package/dist/react/index.js +7252 -0
- package/dist/react/platform-install.js +10 -0
- package/dist/react/server.js +137 -0
- package/dist/stylex-manifest.json +1 -0
- package/dist/stylex.css +5392 -0
- package/dist/syntax-highlighting.js +15 -0
- package/dist/testing.js +250 -0
- package/package.json +377 -0
- package/portfolio-inventory.json +31 -0
- package/scripts/check-lantern-material-snapshot.d.mts +26 -0
- package/scripts/check-lantern-material-snapshot.mjs +126 -0
- package/scripts/check-marketing-snapshot.d.mts +12 -0
- package/scripts/check-marketing-snapshot.mjs +58 -0
- package/scripts/lantern-material-snapshot.ts +101 -0
- package/scripts/marketing-textures.ts +48 -0
- package/scripts/paper-theme-snapshot.ts +97 -0
- package/scripts/product-marketing-snapshot.ts +149 -0
- package/src/appearance-menu.css +266 -0
- package/src/appearance.ts +32 -0
- package/src/article-html.ts +269 -0
- package/src/article.ts +686 -0
- package/src/browser/appearance-menu.ts +604 -0
- package/src/browser/artifact-share.ts +164 -0
- package/src/browser/design-palette.ts +197 -0
- package/src/browser/foil.ts +124 -0
- package/src/browser/hero-light.ts +11 -0
- package/src/browser/index.ts +29 -0
- package/src/browser/status-field.ts +432 -0
- package/src/browser/status-page.ts +100 -0
- package/src/browser/sticky-offset.ts +82 -0
- package/src/browser/theme-color-sync.ts +213 -0
- package/src/charts.css +7 -0
- package/src/compiler-components.css +34 -0
- package/src/compiler-foundation.css +18 -0
- package/src/compiler-palettes.css +7 -0
- package/src/compiler-tokens.css +342 -0
- package/src/components.css +3 -0
- package/src/design-gallery.css +431 -0
- package/src/effects.css +14 -0
- package/src/fonts/geist-mono/GeistMono[wght].woff2 +0 -0
- package/src/fonts/geist-mono/OFL.txt +93 -0
- package/src/fonts/geist-mono/PROVENANCE.md +12 -0
- package/src/fonts/instrument-serif/OFL.txt +93 -0
- package/src/fonts/instrument-serif/UPSTREAM.md +14 -0
- package/src/fonts/instrument-serif/instrument-serif-latin-400.woff2 +0 -0
- package/src/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/fonts/nebula-sans/NebulaSans-Black.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BlackItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BoldItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BookItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Light.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-LightItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Medium.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-MediumItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Semibold.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-SemiboldItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/fonts/nebula-sans/social-fonts.generated.ts +3873 -0
- package/src/fonts.css +106 -0
- package/src/icons/act60/act60.svg +5 -0
- package/src/icons/act60/application.svg +9 -0
- package/src/icons/act60/chapter-compare.svg +3 -0
- package/src/icons/act60/decree-residence.svg +6 -0
- package/src/icons/act60/estimate.svg +7 -0
- package/src/icons/act60/export-services.svg +4 -0
- package/src/icons/act60/investor.svg +13 -0
- package/src/icons/act60/presence.svg +7 -0
- package/src/icons/aicharts/aicharts.svg +6 -0
- package/src/icons/aicharts/cost-compare.svg +4 -0
- package/src/icons/aicharts/task-audio.svg +7 -0
- package/src/icons/aicharts/task-coding.svg +6 -0
- package/src/icons/aicharts/task-images.svg +3 -0
- package/src/icons/aicharts/task-reasoning.svg +9 -0
- package/src/icons/aicharts/task-research.svg +5 -0
- package/src/icons/aicharts/task-video.svg +4 -0
- package/src/icons/manifest.json +1371 -0
- package/src/icons/platonik/conversation.svg +3 -0
- package/src/icons/platonik/creature.svg +7 -0
- package/src/icons/platonik/economy.svg +4 -0
- package/src/icons/platonik/keep-light.svg +6 -0
- package/src/icons/platonik/platonik.svg +7 -0
- package/src/icons/platonik/spark-route.svg +15 -0
- package/src/icons/receipts/act60-retry.receipt.json +888 -0
- package/src/icons/receipts/act60.receipt.json +1366 -0
- package/src/icons/receipts/aicharts.receipt.json +1546 -0
- package/src/icons/receipts/platonik.receipt.json +1011 -0
- package/src/icons/receipts/roughday.receipt.json +770 -0
- package/src/icons/receipts/shared.receipt.json +1296 -0
- package/src/icons/receipts/slopcamera.receipt.json +1495 -0
- package/src/icons/receipts/soundfish-retry.receipt.json +460 -0
- package/src/icons/receipts/soundfish.receipt.json +1709 -0
- package/src/icons/receipts/sponge.receipt.json +1743 -0
- package/src/icons/receipts/stripe-history-retry.receipt.json +145 -0
- package/src/icons/receipts/stripe-history.receipt.json +1202 -0
- package/src/icons/receipts/wordcell.receipt.json +1924 -0
- package/src/icons/roughday/finance.svg +7 -0
- package/src/icons/roughday/roughday.svg +6 -0
- package/src/icons/roughday/technology.svg +4 -0
- package/src/icons/roughday/world.svg +4 -0
- package/src/icons/sets/act60.json +63 -0
- package/src/icons/sets/aicharts.json +63 -0
- package/src/icons/sets/platonik.json +39 -0
- package/src/icons/sets/roughday.json +25 -0
- package/src/icons/sets/shared.json +12 -0
- package/src/icons/sets/slopcamera.json +47 -0
- package/src/icons/sets/soundfish.json +43 -0
- package/src/icons/sets/sponge.json +51 -0
- package/src/icons/sets/stripe-history.json +39 -0
- package/src/icons/sets/wordcell.json +51 -0
- package/src/icons/shared/agent-skill.svg +4 -0
- package/src/icons/shared/cli.svg +5 -0
- package/src/icons/shared/privacy.svg +3 -0
- package/src/icons/shared/research.svg +7 -0
- package/src/icons/shared/sdk.svg +3 -0
- package/src/icons/slopcamera/diagram.svg +3 -0
- package/src/icons/slopcamera/direct-scene.svg +5 -0
- package/src/icons/slopcamera/edit-video.svg +17 -0
- package/src/icons/slopcamera/mcp.svg +3 -0
- package/src/icons/slopcamera/native-world.svg +3 -0
- package/src/icons/slopcamera/slopcamera.svg +11 -0
- package/src/icons/soundfish/agent-ui.svg +7 -0
- package/src/icons/soundfish/collaborate.svg +5 -0
- package/src/icons/soundfish/compose.svg +3 -0
- package/src/icons/soundfish/hear.svg +6 -0
- package/src/icons/soundfish/human-ui.svg +6 -0
- package/src/icons/soundfish/share.svg +3 -0
- package/src/icons/soundfish/soundfish.svg +6 -0
- package/src/icons/sponge/api.svg +3 -0
- package/src/icons/sponge/documents.svg +3 -0
- package/src/icons/sponge/library.svg +3 -0
- package/src/icons/sponge/one-question.svg +5 -0
- package/src/icons/sponge/people-direct.svg +5 -0
- package/src/icons/sponge/read.svg +3 -0
- package/src/icons/sponge/save.svg +4 -0
- package/src/icons/sponge/sponge.svg +7 -0
- package/src/icons/stripe-history/company-history.svg +3 -0
- package/src/icons/stripe-history/evidence.svg +4 -0
- package/src/icons/stripe-history/independence.svg +7 -0
- package/src/icons/stripe-history/publications.svg +11 -0
- package/src/icons/stripe-history/sources.svg +6 -0
- package/src/icons/stripe-history/stripe-history.svg +3 -0
- package/src/icons/wordcell/backlinks.svg +6 -0
- package/src/icons/wordcell/capture.svg +6 -0
- package/src/icons/wordcell/git-provenance.svg +4 -0
- package/src/icons/wordcell/kb.svg +4 -0
- package/src/icons/wordcell/markdown.svg +4 -0
- package/src/icons/wordcell/scopes.svg +3 -0
- package/src/icons/wordcell/search.svg +7 -0
- package/src/icons.generated.ts +734 -0
- package/src/icons.ts +51 -0
- package/src/index.ts +275 -0
- package/src/lantern-material.css +213 -0
- package/src/launch.ts +624 -0
- package/src/marketing-assets/UPSTREAM.md +18 -0
- package/src/marketing-assets/cells.svg +1 -0
- package/src/marketing-assets/grain.svg +1 -0
- package/src/marketing-forced-colors.css +12 -0
- package/src/mockups/client.tsx +415 -0
- package/src/mockups/core.tsx +318 -0
- package/src/mockups/frames.tsx +368 -0
- package/src/mockups/index.ts +14 -0
- package/src/mockups/surfaces.tsx +681 -0
- package/src/mockups.css +2607 -0
- package/src/palette-appearance.ts +45 -0
- package/src/palette-bridge.css +124 -0
- package/src/palette-color.ts +41 -0
- package/src/palette-system.css +721 -0
- package/src/palette-themes.ts +23 -0
- package/src/palette-tokens.stylex.ts +519 -0
- package/src/palettes.css +3 -0
- package/src/palettes.ts +161 -0
- package/src/paper-theme.css +244 -0
- package/src/plain-publication.css +1241 -0
- package/src/plain-site.css +397 -0
- package/src/platforms.ts +151 -0
- package/src/portfolio.generated.json +1595 -0
- package/src/portfolio.generated.ts +1596 -0
- package/src/portfolio.ts +226 -0
- package/src/product-marketing-foundation.css +433 -0
- package/src/product-marketing-preset.css +261 -0
- package/src/product-marketing.css +2186 -0
- package/src/provider-marks.generated.ts +112 -0
- package/src/provider-marks.ts +143 -0
- package/src/react/animated-rail-stage.stylex.ts +15 -0
- package/src/react/animated-rail-stage.tsx +51 -0
- package/src/react/app-shell.stylex.ts +104 -0
- package/src/react/app-shell.tsx +121 -0
- package/src/react/article.tsx +602 -0
- package/src/react/aurora-dots-background.tsx +34 -0
- package/src/react/charts.stylex.ts +563 -0
- package/src/react/charts.tsx +610 -0
- package/src/react/chat.stylex.ts +34 -0
- package/src/react/chat.tsx +155 -0
- package/src/react/design-gallery.tsx +943 -0
- package/src/react/design-palette.stylex.ts +124 -0
- package/src/react/design-palette.tsx +196 -0
- package/src/react/design-theme-context.tsx +41 -0
- package/src/react/effects.stylex.ts +325 -0
- package/src/react/fader.stylex.ts +71 -0
- package/src/react/fader.tsx +151 -0
- package/src/react/foil-card-math.ts +100 -0
- package/src/react/foil-card-surface.stylex.ts +326 -0
- package/src/react/foil-card-surface.tsx +742 -0
- package/src/react/foil-mark.tsx +35 -0
- package/src/react/foil.stylex.ts +163 -0
- package/src/react/haptics.ts +228 -0
- package/src/react/hero-backdrop.tsx +22 -0
- package/src/react/index.ts +38 -0
- package/src/react/keyboard-shortcuts.ts +228 -0
- package/src/react/lantern-material-gallery.tsx +166 -0
- package/src/react/lantern-material.stylex.ts +39 -0
- package/src/react/launch-beats.tsx +73 -0
- package/src/react/navigation-rail.stylex.ts +130 -0
- package/src/react/navigation-rail.tsx +228 -0
- package/src/react/particle-halo.tsx +115 -0
- package/src/react/phaser-dots.tsx +501 -0
- package/src/react/platform-icons.tsx +94 -0
- package/src/react/platform-install.stylex.ts +385 -0
- package/src/react/platform-install.tsx +389 -0
- package/src/react/playback-transport.stylex.ts +14 -0
- package/src/react/playback-transport.tsx +112 -0
- package/src/react/procedural-backdrop.tsx +192 -0
- package/src/react/procedural-recipe.ts +323 -0
- package/src/react/product-marketing.stylex.ts +4469 -0
- package/src/react/product-marketing.tsx +1611 -0
- package/src/react/production-data-preview-notice.stylex.ts +40 -0
- package/src/react/production-data-preview-notice.tsx +34 -0
- package/src/react/provider-mark.stylex.ts +132 -0
- package/src/react/provider-mark.tsx +120 -0
- package/src/react/relative-time.tsx +117 -0
- package/src/react/route-state.stylex.ts +39 -0
- package/src/react/route-state.tsx +350 -0
- package/src/react/server.ts +11 -0
- package/src/react/social-kit-panel.tsx +103 -0
- package/src/react/sticky-offset.tsx +33 -0
- package/src/react/surfaces.stylex.ts +179 -0
- package/src/react/surfaces.tsx +330 -0
- package/src/react/syntax-code.tsx +33 -0
- package/src/react/theme-color-sync.ts +1 -0
- package/src/react/theme-resolution.ts +11 -0
- package/src/react/theme.stylex.ts +276 -0
- package/src/react/theme.tsx +402 -0
- package/src/reading.css +72 -0
- package/src/relative-time.ts +185 -0
- package/src/reset.css +2 -0
- package/src/site-shell.css +16 -0
- package/src/status-page-html.ts +63 -0
- package/src/status-page.css +276 -0
- package/src/status-page.ts +323 -0
- package/src/styles.css +21 -0
- package/src/syntax-code-lexers.ts +303 -0
- package/src/syntax-highlighting.css +78 -0
- package/src/syntax-highlighting.ts +477 -0
- package/src/syntax-tokens.ts +33 -0
- package/src/testing.ts +307 -0
- package/src/tokens.css +3 -0
- package/src/typography.css +86 -0
- package/vendor/evilcharts/LICENSE +21 -0
- package/vendor/evilcharts/UPSTREAM.md +14 -0
- package/vendor/platform-marks/LICENSE +30 -0
- package/vendor/platform-marks/UPSTREAM.md +15 -0
- package/vendor/platform-marks/apple.svg +1 -0
- package/vendor/platform-marks/linux.svg +1 -0
- package/vendor/provider-marks/CRUSH-LICENSE.md +134 -0
- package/vendor/provider-marks/LICENSE +21 -0
- package/vendor/provider-marks/UPSTREAM.md +27 -0
- package/vendor/provider-marks/aider.svg +3 -0
- package/vendor/provider-marks/crush-heartbit.svg +8 -0
- package/vendor/provider-marks/lobehub/alibabacloud-color.svg +1 -0
- package/vendor/provider-marks/lobehub/alibabacloud.svg +1 -0
- package/vendor/provider-marks/lobehub/amp-color.svg +1 -0
- package/vendor/provider-marks/lobehub/amp.svg +1 -0
- package/vendor/provider-marks/lobehub/anthropic.svg +1 -0
- package/vendor/provider-marks/lobehub/claude-color.svg +1 -0
- package/vendor/provider-marks/lobehub/claude.svg +1 -0
- package/vendor/provider-marks/lobehub/claudecode-color.svg +1 -0
- package/vendor/provider-marks/lobehub/claudecode.svg +1 -0
- package/vendor/provider-marks/lobehub/codex-color.svg +1 -0
- package/vendor/provider-marks/lobehub/codex.svg +1 -0
- package/vendor/provider-marks/lobehub/cursor.svg +1 -0
- package/vendor/provider-marks/lobehub/deepseek-color.svg +1 -0
- package/vendor/provider-marks/lobehub/deepseek.svg +1 -0
- package/vendor/provider-marks/lobehub/devin-color.svg +1 -0
- package/vendor/provider-marks/lobehub/devin.svg +1 -0
- package/vendor/provider-marks/lobehub/gemini-color.svg +1 -0
- package/vendor/provider-marks/lobehub/gemini.svg +1 -0
- package/vendor/provider-marks/lobehub/geminicli-color.svg +1 -0
- package/vendor/provider-marks/lobehub/geminicli.svg +1 -0
- package/vendor/provider-marks/lobehub/goose.svg +1 -0
- package/vendor/provider-marks/lobehub/meta-color.svg +1 -0
- package/vendor/provider-marks/lobehub/meta.svg +1 -0
- package/vendor/provider-marks/lobehub/mistral-color.svg +1 -0
- package/vendor/provider-marks/lobehub/mistral.svg +1 -0
- package/vendor/provider-marks/lobehub/moonshot.svg +1 -0
- package/vendor/provider-marks/lobehub/nvidia-color.svg +1 -0
- package/vendor/provider-marks/lobehub/nvidia.svg +1 -0
- package/vendor/provider-marks/lobehub/openai.svg +1 -0
- package/vendor/provider-marks/lobehub/opencode.svg +1 -0
- package/vendor/provider-marks/lobehub/perplexity-color.svg +1 -0
- package/vendor/provider-marks/lobehub/perplexity.svg +1 -0
- package/vendor/provider-marks/lobehub/qwen-color.svg +1 -0
- package/vendor/provider-marks/lobehub/qwen.svg +1 -0
- package/vendor/provider-marks/lobehub/xai.svg +1 -0
- package/vendor/provider-marks/lobehub/zai.svg +1 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// src/launch.ts
|
|
2
|
+
var launchBeatParts = ["what", "does", "how", "who", "vision", "limits", "status"];
|
|
3
|
+
var launchPostOnlyParts = Object.freeze(["limits"]);
|
|
4
|
+
var LAUNCH_LIMITS = Object.freeze({
|
|
5
|
+
beatsMin: 7,
|
|
6
|
+
beatsMax: 10,
|
|
7
|
+
headline: 70,
|
|
8
|
+
post: 250,
|
|
9
|
+
alt: 125,
|
|
10
|
+
x: 280,
|
|
11
|
+
xUrl: 23,
|
|
12
|
+
bluesky: 300,
|
|
13
|
+
threads: 500,
|
|
14
|
+
linkedin: 3000,
|
|
15
|
+
linkedinHook: 210,
|
|
16
|
+
productHuntTagline: 60,
|
|
17
|
+
productHuntDescription: 260
|
|
18
|
+
});
|
|
19
|
+
var LAUNCH_BANNED_WORDS = Object.freeze(["actually", "amid", "blazing", "cutting-edge", "delve", "elevate", "empower", "game-changer", "genuinely", "harness", "in today's world", "journey", "landscape", "leverage", "notable", "pivotal", "powerful", "realm", "revolutionary", "revolutionize", "robust", "seamless", "seamlessly", "showcase", "significant", "supercharge", "tapestry", "testament", "underscore", "unlock"]);
|
|
20
|
+
var LAUNCH_INTERNAL_WORDS = Object.freeze(["admission", "admitted", "attest", "bounded", "boundary", "custody", "gate", "lane", "lease", "manifest", "projection", "qualification", "quarantined", "receipt", "settlement", "surface"]);
|
|
21
|
+
var HOOK_PATTERNS = [/\bhere'?s why\b/iu, /\bhere'?s how\b/iu, /\ba thread\b/iu, /\bthread below\b/iu, /\bwait (for|until)\b/iu, /\byou won'?t believe\b/iu];
|
|
22
|
+
var THREAD_MARKER = /(^|\s)\d+\s*\/\s*\d*(\s|$)|\u{1F9F5}/u;
|
|
23
|
+
var INSTALL_PATTERN = /\b(install|installs|installing|npm i|npx|bunx|pip|brew|download|get it (now|here)|try it (now|today)|sign up)\b/iu;
|
|
24
|
+
var CURL_WORD = /\bcurl\s/iu;
|
|
25
|
+
var PIPE_TO_SHELL = /\|\s*(?:ba|z)?sh\b/iu;
|
|
26
|
+
function asksToInstall(text) {
|
|
27
|
+
if (INSTALL_PATTERN.test(text))
|
|
28
|
+
return true;
|
|
29
|
+
const curl = CURL_WORD.exec(text);
|
|
30
|
+
return curl !== null && PIPE_TO_SHELL.test(text.slice(curl.index));
|
|
31
|
+
}
|
|
32
|
+
var TRACKING_PARAM = /[?&](utm_[a-z]+|ref|ref_src|fbclid|gclid|mc_[a-z]+|s)=/iu;
|
|
33
|
+
var EMOJI = /[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{2300}-\u{23FF}]/u;
|
|
34
|
+
var HASHTAG = /(^|\s)#[A-Za-z0-9_À-ɏͰ-ϿЀ-ӿ]+/u;
|
|
35
|
+
var PLACEHOLDER = /\{([a-z][a-zA-Z0-9_.-]*)\}/gu;
|
|
36
|
+
var BEAT_ID = /^[a-z0-9][a-z0-9-]*$/u;
|
|
37
|
+
var STATUS_LABEL = /^(In development|Preview|Beta|Latest release: v\d+\.\d+\.\d+|Paused|Retired)$/u;
|
|
38
|
+
var SCHEME_URL = /\bhttps?:\/\/[^\s]+/giu;
|
|
39
|
+
var BARE_DOMAIN = /\b[a-z0-9][a-z0-9-]*(\.[a-z0-9-]+)*\.(com|net|org|dev|app|io|ai|co|sh|so|xyz|me|site|page|tech)(\/[^\s]*)?\b/giu;
|
|
40
|
+
|
|
41
|
+
class LaunchKitError extends Error {
|
|
42
|
+
problems;
|
|
43
|
+
constructor(problems) {
|
|
44
|
+
super(`Launch kit check failed:
|
|
45
|
+
- ${problems.join(`
|
|
46
|
+
- `)}`);
|
|
47
|
+
this.name = "LaunchKitError";
|
|
48
|
+
this.problems = Object.freeze([...problems]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function escapeRegExp(value) {
|
|
52
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
53
|
+
}
|
|
54
|
+
function wordPattern(words) {
|
|
55
|
+
const alternatives = words.map((word) => escapeRegExp(word).replace(/'/gu, "['’]").replace(/-/gu, "[- ]?"));
|
|
56
|
+
return new RegExp(`(?<![A-Za-z0-9\\u00C0-\\u024F])(${alternatives.join("|")})(s|es|d|ed|ing)?(?![A-Za-z0-9\\u00C0-\\u024F])`, "iu");
|
|
57
|
+
}
|
|
58
|
+
var BANNED_PATTERN = wordPattern(LAUNCH_BANNED_WORDS);
|
|
59
|
+
var INTERNAL_PATTERN = wordPattern(LAUNCH_INTERNAL_WORDS);
|
|
60
|
+
function launchCopyProblems(text, label = "text") {
|
|
61
|
+
const problems = [];
|
|
62
|
+
const banned = BANNED_PATTERN.exec(text);
|
|
63
|
+
if (banned !== null)
|
|
64
|
+
problems.push(`${label} uses the banned word "${banned[0]}".`);
|
|
65
|
+
const internal = INTERNAL_PATTERN.exec(text);
|
|
66
|
+
if (internal !== null)
|
|
67
|
+
problems.push(`${label} uses the internal word "${internal[0]}".`);
|
|
68
|
+
if (text.includes("—"))
|
|
69
|
+
problems.push(`${label} contains an em dash.`);
|
|
70
|
+
if (text.includes("!"))
|
|
71
|
+
problems.push(`${label} contains an exclamation mark.`);
|
|
72
|
+
if (/\?["'”’)]*$/u.test(text.trim()))
|
|
73
|
+
problems.push(`${label} ends with a question mark.`);
|
|
74
|
+
if (EMOJI.test(text))
|
|
75
|
+
problems.push(`${label} contains an emoji.`);
|
|
76
|
+
if (HASHTAG.test(text))
|
|
77
|
+
problems.push(`${label} contains a hashtag.`);
|
|
78
|
+
if (THREAD_MARKER.test(text))
|
|
79
|
+
problems.push(`${label} contains a thread marker.`);
|
|
80
|
+
return problems;
|
|
81
|
+
}
|
|
82
|
+
function codePoints(text) {
|
|
83
|
+
return Array.from(text.normalize("NFC")).length;
|
|
84
|
+
}
|
|
85
|
+
function graphemes(text) {
|
|
86
|
+
const segmenter = new Intl.Segmenter("en", {
|
|
87
|
+
granularity: "grapheme"
|
|
88
|
+
});
|
|
89
|
+
return Array.from(segmenter.segment(text.normalize("NFC"))).length;
|
|
90
|
+
}
|
|
91
|
+
function xWeight(codePoint) {
|
|
92
|
+
const narrow = codePoint >= 0 && codePoint <= 4351 || codePoint >= 8192 && codePoint <= 8205 || codePoint >= 8208 && codePoint <= 8223 || codePoint >= 8242 && codePoint <= 8247;
|
|
93
|
+
return narrow ? 1 : 2;
|
|
94
|
+
}
|
|
95
|
+
function xPostLength(text) {
|
|
96
|
+
let links = 0;
|
|
97
|
+
const withoutLinks = text.normalize("NFC").replace(SCHEME_URL, () => {
|
|
98
|
+
links += 1;
|
|
99
|
+
return "";
|
|
100
|
+
}).replace(BARE_DOMAIN, () => {
|
|
101
|
+
links += 1;
|
|
102
|
+
return "";
|
|
103
|
+
});
|
|
104
|
+
let length = links * LAUNCH_LIMITS.xUrl;
|
|
105
|
+
const segmenter = new Intl.Segmenter("en", {
|
|
106
|
+
granularity: "grapheme"
|
|
107
|
+
});
|
|
108
|
+
for (const {
|
|
109
|
+
segment
|
|
110
|
+
} of segmenter.segment(withoutLinks)) {
|
|
111
|
+
if (EMOJI.test(segment)) {
|
|
112
|
+
length += 2;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const char of segment)
|
|
116
|
+
length += xWeight(char.codePointAt(0) ?? 0);
|
|
117
|
+
}
|
|
118
|
+
return length;
|
|
119
|
+
}
|
|
120
|
+
function blueskyPostLength(text) {
|
|
121
|
+
return graphemes(text);
|
|
122
|
+
}
|
|
123
|
+
function characterLength(text) {
|
|
124
|
+
return codePoints(text);
|
|
125
|
+
}
|
|
126
|
+
function visualKey(visual) {
|
|
127
|
+
switch (visual.kind) {
|
|
128
|
+
case "mockup":
|
|
129
|
+
return `mockup:${visual.id}:${JSON.stringify(Object.entries(visual.state).toSorted(([a], [b]) => a.localeCompare(b)))}`;
|
|
130
|
+
case "clip":
|
|
131
|
+
return `clip:${visual.scene}`;
|
|
132
|
+
case "diagram":
|
|
133
|
+
return `diagram:${visual.src}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function isSocialBeat(beat) {
|
|
137
|
+
return beat.social !== false && !launchPostOnlyParts.includes(beat.part);
|
|
138
|
+
}
|
|
139
|
+
function socialBeats(beats) {
|
|
140
|
+
return beats.filter(isSocialBeat);
|
|
141
|
+
}
|
|
142
|
+
function socialPostText(beat) {
|
|
143
|
+
return (beat.socialPost ?? beat.post).trim();
|
|
144
|
+
}
|
|
145
|
+
function beatShapeProblems(beats) {
|
|
146
|
+
const problems = [];
|
|
147
|
+
if (beats.length < LAUNCH_LIMITS.beatsMin || beats.length > LAUNCH_LIMITS.beatsMax) {
|
|
148
|
+
problems.push(`A launch post has ${LAUNCH_LIMITS.beatsMin} to ${LAUNCH_LIMITS.beatsMax} beats; this one has ${beats.length}.`);
|
|
149
|
+
}
|
|
150
|
+
const ids = new Set;
|
|
151
|
+
const visuals = new Set;
|
|
152
|
+
let previous = -1;
|
|
153
|
+
const counts = new Map;
|
|
154
|
+
for (const beat of beats) {
|
|
155
|
+
const label = `Beat "${beat.id}"`;
|
|
156
|
+
if (!BEAT_ID.test(beat.id))
|
|
157
|
+
problems.push(`${label} needs a lowercase slug id.`);
|
|
158
|
+
if (ids.has(beat.id))
|
|
159
|
+
problems.push(`${label} is repeated.`);
|
|
160
|
+
ids.add(beat.id);
|
|
161
|
+
const order = launchBeatParts.indexOf(beat.part);
|
|
162
|
+
if (order < 0)
|
|
163
|
+
problems.push(`${label} has an unknown part "${String(beat.part)}".`);
|
|
164
|
+
else if (order < previous)
|
|
165
|
+
problems.push(`${label} (${beat.part}) is out of order; beats run ${launchBeatParts.join(", ")}.`);
|
|
166
|
+
previous = Math.max(previous, order);
|
|
167
|
+
counts.set(beat.part, (counts.get(beat.part) ?? 0) + 1);
|
|
168
|
+
const key = visualKey(beat.visual);
|
|
169
|
+
if (visuals.has(key))
|
|
170
|
+
problems.push(`${label} reuses another beat's visual; every beat needs its own.`);
|
|
171
|
+
visuals.add(key);
|
|
172
|
+
if (beat.social === false && (beat.part === "what" || beat.part === "status")) {
|
|
173
|
+
problems.push(`${label} is the ${beat.part === "what" ? "first" : "last"} social post; it cannot be marked social: false.`);
|
|
174
|
+
}
|
|
175
|
+
if (beat.socialPost !== undefined && !isSocialBeat(beat)) {
|
|
176
|
+
problems.push(`${label} never reaches a social post, so it cannot carry a socialPost.`);
|
|
177
|
+
}
|
|
178
|
+
if (beat.detailHref !== undefined && !/^(\/(?!\/)|https:\/\/)/u.test(beat.detailHref)) {
|
|
179
|
+
problems.push(`${label} detailHref must be a site path or an https URL.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const part of launchBeatParts) {
|
|
183
|
+
const count = counts.get(part) ?? 0;
|
|
184
|
+
if (part === "does" ? count < 1 : count !== 1) {
|
|
185
|
+
problems.push(part === "does" ? 'A launch post needs at least one "does" beat.' : `A launch post needs exactly one "${part}" beat; it has ${count}.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return problems;
|
|
189
|
+
}
|
|
190
|
+
function beatTextProblems(beat) {
|
|
191
|
+
const label = `Beat "${beat.id}"`;
|
|
192
|
+
const problems = [];
|
|
193
|
+
const headline = beat.headline.trim();
|
|
194
|
+
if (headline.length === 0)
|
|
195
|
+
problems.push(`${label} needs a headline.`);
|
|
196
|
+
if (characterLength(headline) > LAUNCH_LIMITS.headline)
|
|
197
|
+
problems.push(`${label} headline is longer than ${LAUNCH_LIMITS.headline} characters.`);
|
|
198
|
+
if (/\.$/u.test(headline))
|
|
199
|
+
problems.push(`${label} headline ends with a period.`);
|
|
200
|
+
if (/^[a-zß-öø-ÿ]/u.test(headline))
|
|
201
|
+
problems.push(`${label} headline starts in lowercase.`);
|
|
202
|
+
if (/\s[A-ZÀ-Þ][a-zß-ÿ]+\s[A-ZÀ-Þ][a-zß-ÿ]+\s[A-ZÀ-Þ][a-zß-ÿ]+/u.test(headline))
|
|
203
|
+
problems.push(`${label} headline looks like title case; use sentence case.`);
|
|
204
|
+
if (characterLength(beat.post) > LAUNCH_LIMITS.post)
|
|
205
|
+
problems.push(`${label} post is longer than ${LAUNCH_LIMITS.post} characters.`);
|
|
206
|
+
if (beat.post.trim().length === 0)
|
|
207
|
+
problems.push(`${label} needs a post.`);
|
|
208
|
+
if (/\b(above|below|next post|previous post|as mentioned|see thread)\b/iu.test(beat.post)) {
|
|
209
|
+
problems.push(`${label} post refers to another post; each post stands alone.`);
|
|
210
|
+
}
|
|
211
|
+
if (characterLength(beat.alt) > LAUNCH_LIMITS.alt)
|
|
212
|
+
problems.push(`${label} alt text is longer than ${LAUNCH_LIMITS.alt} characters.`);
|
|
213
|
+
if (beat.alt.trim().length === 0)
|
|
214
|
+
problems.push(`${label} needs alt text.`);
|
|
215
|
+
if (beat.visual.kind === "mockup" && !/\billustration\b/iu.test(beat.alt)) {
|
|
216
|
+
problems.push(`${label} shows a mockup, so its alt text says "Illustration".`);
|
|
217
|
+
}
|
|
218
|
+
if (beat.socialPost !== undefined) {
|
|
219
|
+
if (beat.socialPost.trim().length === 0)
|
|
220
|
+
problems.push(`${label} socialPost is empty; leave it out to use the post.`);
|
|
221
|
+
if (characterLength(beat.socialPost) > LAUNCH_LIMITS.post)
|
|
222
|
+
problems.push(`${label} socialPost is longer than ${LAUNCH_LIMITS.post} characters.`);
|
|
223
|
+
if (/\b(above|below|next post|previous post|as mentioned|see thread)\b/iu.test(beat.socialPost)) {
|
|
224
|
+
problems.push(`${label} socialPost refers to another post; each post stands alone.`);
|
|
225
|
+
}
|
|
226
|
+
problems.push(...launchCopyProblems(beat.socialPost, `${label} socialPost`));
|
|
227
|
+
}
|
|
228
|
+
for (const [field, text] of [["headline", beat.headline], ["post", beat.post], ["alt", beat.alt]]) {
|
|
229
|
+
problems.push(...launchCopyProblems(text, `${label} ${field}`));
|
|
230
|
+
}
|
|
231
|
+
if (beat.part === "what") {
|
|
232
|
+
const first = socialPostText(beat);
|
|
233
|
+
if (first.includes("?"))
|
|
234
|
+
problems.push(`${label} is the first post; it states what the product does and asks nothing.`);
|
|
235
|
+
for (const pattern of HOOK_PATTERNS)
|
|
236
|
+
if (pattern.test(first))
|
|
237
|
+
problems.push(`${label} opens with a teaser; state what the product does.`);
|
|
238
|
+
}
|
|
239
|
+
return problems;
|
|
240
|
+
}
|
|
241
|
+
function assertLaunchBeats(beats) {
|
|
242
|
+
const problems = [...beatShapeProblems(beats), ...beats.flatMap(beatTextProblems)];
|
|
243
|
+
if (problems.length > 0)
|
|
244
|
+
throw new LaunchKitError(problems);
|
|
245
|
+
}
|
|
246
|
+
function launchPlaceholders(text) {
|
|
247
|
+
return [...text.matchAll(PLACEHOLDER)].map((match) => match[1] ?? "");
|
|
248
|
+
}
|
|
249
|
+
function resolveLaunchBeats(beats, facts, options = {}) {
|
|
250
|
+
const problems = [];
|
|
251
|
+
const allowed = options.allowNumerals ?? [];
|
|
252
|
+
for (const [key, fact] of Object.entries(facts)) {
|
|
253
|
+
if (fact.value.trim().length === 0)
|
|
254
|
+
problems.push(`Fact "${key}" has no value.`);
|
|
255
|
+
if (fact.source.trim().length === 0)
|
|
256
|
+
problems.push(`Fact "${key}" has no source.`);
|
|
257
|
+
}
|
|
258
|
+
const resolved = beats.map((beat) => {
|
|
259
|
+
const listed = new Set(beat.facts ?? []);
|
|
260
|
+
for (const key of listed)
|
|
261
|
+
if (!Object.hasOwn(facts, key))
|
|
262
|
+
problems.push(`Beat "${beat.id}" lists the unknown fact "${key}".`);
|
|
263
|
+
const fill = (text, field) => {
|
|
264
|
+
let authored = text.replace(PLACEHOLDER, " ");
|
|
265
|
+
for (const name of allowed)
|
|
266
|
+
authored = authored.split(name).join(" ");
|
|
267
|
+
authored = authored.replace(SCHEME_URL, " ");
|
|
268
|
+
if (/\d/u.test(authored))
|
|
269
|
+
problems.push(`Beat "${beat.id}" ${field} types a number; put it in the facts module and use a {placeholder}.`);
|
|
270
|
+
return text.replace(PLACEHOLDER, (match, key) => {
|
|
271
|
+
if (!listed.has(key)) {
|
|
272
|
+
problems.push(`Beat "${beat.id}" ${field} uses {${key}} without listing it in facts.`);
|
|
273
|
+
return match;
|
|
274
|
+
}
|
|
275
|
+
return facts[key]?.value ?? match;
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
return Object.freeze({
|
|
279
|
+
...beat,
|
|
280
|
+
headline: fill(beat.headline, "headline"),
|
|
281
|
+
post: fill(beat.post, "post"),
|
|
282
|
+
alt: fill(beat.alt, "alt"),
|
|
283
|
+
...beat.socialPost === undefined ? {} : {
|
|
284
|
+
socialPost: fill(beat.socialPost, "socialPost")
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
if (problems.length > 0)
|
|
289
|
+
throw new LaunchKitError(problems);
|
|
290
|
+
return Object.freeze(resolved);
|
|
291
|
+
}
|
|
292
|
+
function assertCanonicalUrl(url) {
|
|
293
|
+
const parsed = /^([a-z][a-z0-9+.-]*):\/\/([^/?#\s]+)(\/[^?#\s]*)?(\?[^#\s]*)?(#\S*)?$/iu.exec(url);
|
|
294
|
+
if (parsed === null)
|
|
295
|
+
throw new LaunchKitError([`The canonical URL "${url}" is not a URL.`]);
|
|
296
|
+
const [, scheme = "", , , search = "", hash = ""] = parsed;
|
|
297
|
+
const problems = [];
|
|
298
|
+
if (scheme.toLowerCase() !== "https")
|
|
299
|
+
problems.push("The canonical URL must use https.");
|
|
300
|
+
if (TRACKING_PARAM.test(search))
|
|
301
|
+
problems.push("The canonical URL must not carry tracking parameters.");
|
|
302
|
+
if (hash !== "")
|
|
303
|
+
problems.push("The canonical URL must point at the post, not an anchor.");
|
|
304
|
+
if (problems.length > 0)
|
|
305
|
+
throw new LaunchKitError(problems);
|
|
306
|
+
}
|
|
307
|
+
function buildSocialKit(beats, messaging, release, canonicalUrl) {
|
|
308
|
+
assertLaunchBeats(beats);
|
|
309
|
+
assertCanonicalUrl(canonicalUrl);
|
|
310
|
+
const unresolved = beats.filter((beat) => launchPlaceholders(`${beat.headline} ${beat.post} ${beat.alt} ${beat.socialPost ?? ""}`).length > 0);
|
|
311
|
+
if (unresolved.length > 0) {
|
|
312
|
+
throw new LaunchKitError(unresolved.map((beat) => `Beat "${beat.id}" still has placeholders; run resolveLaunchBeats first.`));
|
|
313
|
+
}
|
|
314
|
+
const cut = socialBeats(beats);
|
|
315
|
+
const posts = cut.map((beat, index) => index === cut.length - 1 ? `${socialPostText(beat)}
|
|
316
|
+
|
|
317
|
+
${canonicalUrl}` : socialPostText(beat));
|
|
318
|
+
const sources = {
|
|
319
|
+
"productHunt.tagline": "messaging.tagline",
|
|
320
|
+
"productHunt.description": "messaging.meta",
|
|
321
|
+
"productHunt.tags": "release.tags",
|
|
322
|
+
status: "release.status",
|
|
323
|
+
url: "canonicalUrl"
|
|
324
|
+
};
|
|
325
|
+
cut.forEach((beat, index) => {
|
|
326
|
+
for (const channel of ["x", "bluesky", "threads"])
|
|
327
|
+
sources[`${channel}.${index}`] = `beat:${beat.id}`;
|
|
328
|
+
});
|
|
329
|
+
const linkedin = posts.join(`
|
|
330
|
+
|
|
331
|
+
`);
|
|
332
|
+
sources.linkedin = cut.map((beat) => `beat:${beat.id}`).join(",");
|
|
333
|
+
const showHnFacts = [messaging.tagline, ...cut.filter((beat) => beat.part !== "vision").map(socialPostText), `${release.status}. ${canonicalUrl}`];
|
|
334
|
+
return Object.freeze({
|
|
335
|
+
x: Object.freeze([...posts]),
|
|
336
|
+
bluesky: Object.freeze([...posts]),
|
|
337
|
+
threads: Object.freeze([...posts]),
|
|
338
|
+
linkedin,
|
|
339
|
+
productHunt: Object.freeze({
|
|
340
|
+
tagline: messaging.tagline,
|
|
341
|
+
description: messaging.meta,
|
|
342
|
+
tags: Object.freeze([...release.tags ?? []])
|
|
343
|
+
}),
|
|
344
|
+
showHnFacts: Object.freeze(showHnFacts),
|
|
345
|
+
sources: Object.freeze(sources)
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function assertLaunchKit(beats, kit, options) {
|
|
349
|
+
const problems = [];
|
|
350
|
+
try {
|
|
351
|
+
assertLaunchBeats(beats);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (error instanceof LaunchKitError)
|
|
354
|
+
problems.push(...error.problems);
|
|
355
|
+
else
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
if (!STATUS_LABEL.test(options.status))
|
|
359
|
+
problems.push(`"${options.status}" is not a STYLE.md status label.`);
|
|
360
|
+
if (options.canonicalUrl !== undefined) {
|
|
361
|
+
try {
|
|
362
|
+
assertCanonicalUrl(options.canonicalUrl);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (error instanceof LaunchKitError)
|
|
365
|
+
problems.push(...error.problems);
|
|
366
|
+
else
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const cut = socialBeats(beats);
|
|
371
|
+
const postOnly = beats.filter((beat) => !isSocialBeat(beat)).map((beat) => [beat.id, beat.post.trim()]).filter(([, text]) => text.length > 0);
|
|
372
|
+
const forbidden = (options.forbiddenNames ?? []).filter((name) => name.trim().length > 0);
|
|
373
|
+
const forbiddenPattern = forbidden.length === 0 ? null : wordPattern(forbidden);
|
|
374
|
+
const checkText = (text, label) => {
|
|
375
|
+
problems.push(...launchCopyProblems(text, label));
|
|
376
|
+
if (forbiddenPattern !== null) {
|
|
377
|
+
const hit = forbiddenPattern.exec(text);
|
|
378
|
+
if (hit !== null)
|
|
379
|
+
problems.push(`${label} names "${hit[0]}"; comparisons belong on comparison pages.`);
|
|
380
|
+
}
|
|
381
|
+
if (!options.publicInstall && asksToInstall(text)) {
|
|
382
|
+
problems.push(`${label} asks readers to install, but the release has no public install.`);
|
|
383
|
+
}
|
|
384
|
+
for (const match of text.matchAll(SCHEME_URL)) {
|
|
385
|
+
if (TRACKING_PARAM.test(match[0]))
|
|
386
|
+
problems.push(`${label} links with tracking parameters.`);
|
|
387
|
+
}
|
|
388
|
+
for (const [id, post] of postOnly) {
|
|
389
|
+
if (text.includes(post))
|
|
390
|
+
problems.push(`${label} carries beat "${id}", which stays in the launch post; social posts carry no caveats or limits.`);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
const threads = [["x", kit.x, xPostLength, LAUNCH_LIMITS.x], ["bluesky", kit.bluesky, blueskyPostLength, LAUNCH_LIMITS.bluesky], ["threads", kit.threads, characterLength, LAUNCH_LIMITS.threads]];
|
|
394
|
+
for (const [channel, posts, measure, limit] of threads) {
|
|
395
|
+
if (posts.length === 0)
|
|
396
|
+
problems.push(`The ${channel} thread is empty.`);
|
|
397
|
+
if (posts.length > cut.length)
|
|
398
|
+
problems.push(`The ${channel} thread has more posts than social beats.`);
|
|
399
|
+
posts.forEach((post, index) => {
|
|
400
|
+
const label = `${channel} post ${index + 1}`;
|
|
401
|
+
const length = measure(post);
|
|
402
|
+
if (length > limit)
|
|
403
|
+
problems.push(`${label} is ${length} long; the limit is ${limit}.`);
|
|
404
|
+
checkText(post, label);
|
|
405
|
+
});
|
|
406
|
+
const last = posts.at(-1) ?? "";
|
|
407
|
+
if (!last.includes(options.status))
|
|
408
|
+
problems.push(`The last ${channel} post must state the status "${options.status}".`);
|
|
409
|
+
if (options.canonicalUrl !== undefined && !last.trimEnd().endsWith(options.canonicalUrl)) {
|
|
410
|
+
problems.push(`The last ${channel} post must end with the canonical URL.`);
|
|
411
|
+
}
|
|
412
|
+
posts.slice(0, -1).forEach((post, index) => {
|
|
413
|
+
if (/\bhttps?:\/\//u.test(post))
|
|
414
|
+
problems.push(`${channel} post ${index + 1} carries a link; only the last post links.`);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (characterLength(kit.linkedin) > LAUNCH_LIMITS.linkedin)
|
|
418
|
+
problems.push(`The LinkedIn post is longer than ${LAUNCH_LIMITS.linkedin} characters.`);
|
|
419
|
+
const hook = kit.linkedin.split(/\n\s*\n/u)[0] ?? "";
|
|
420
|
+
if (characterLength(hook) > LAUNCH_LIMITS.linkedinHook)
|
|
421
|
+
problems.push(`The LinkedIn opening paragraph is longer than ${LAUNCH_LIMITS.linkedinHook} characters.`);
|
|
422
|
+
if (!kit.linkedin.includes(options.status))
|
|
423
|
+
problems.push(`The LinkedIn post must state the status "${options.status}".`);
|
|
424
|
+
checkText(kit.linkedin, "LinkedIn post");
|
|
425
|
+
const {
|
|
426
|
+
productHunt
|
|
427
|
+
} = kit;
|
|
428
|
+
if (characterLength(productHunt.tagline) > LAUNCH_LIMITS.productHuntTagline)
|
|
429
|
+
problems.push(`The Product Hunt tagline is longer than ${LAUNCH_LIMITS.productHuntTagline} characters.`);
|
|
430
|
+
if (characterLength(productHunt.description) > LAUNCH_LIMITS.productHuntDescription) {
|
|
431
|
+
problems.push(`The Product Hunt description is longer than ${LAUNCH_LIMITS.productHuntDescription} characters.`);
|
|
432
|
+
}
|
|
433
|
+
if (options.tagline !== undefined && productHunt.tagline !== options.tagline)
|
|
434
|
+
problems.push("The Product Hunt tagline must be the messaging tagline.");
|
|
435
|
+
if (kit.sources["productHunt.tagline"] !== "messaging.tagline")
|
|
436
|
+
problems.push("The Product Hunt tagline must come from the messaging record.");
|
|
437
|
+
if (kit.sources.status !== "release.status")
|
|
438
|
+
problems.push("The status must come from the release record.");
|
|
439
|
+
checkText(productHunt.tagline, "Product Hunt tagline");
|
|
440
|
+
checkText(productHunt.description, "Product Hunt description");
|
|
441
|
+
kit.showHnFacts.forEach((fact, index) => checkText(fact, `fact ${index + 1}`));
|
|
442
|
+
if (problems.length > 0)
|
|
443
|
+
throw new LaunchKitError([...new Set(problems)]);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export { launchBeatParts, launchPostOnlyParts, LAUNCH_LIMITS, LAUNCH_BANNED_WORDS, LAUNCH_INTERNAL_WORDS, LaunchKitError, launchCopyProblems, xPostLength, blueskyPostLength, characterLength, isSocialBeat, socialBeats, socialPostText, assertLaunchBeats, launchPlaceholders, resolveLaunchBeats, buildSocialKit, assertLaunchKit };
|