@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,389 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useId, useRef, useState, type KeyboardEvent, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
detectPlatform,
|
|
7
|
+
isKnownPlatformId,
|
|
8
|
+
isPlatformId,
|
|
9
|
+
matchDetectedPlatform,
|
|
10
|
+
platformLabel,
|
|
11
|
+
platformMark,
|
|
12
|
+
type PlatformId,
|
|
13
|
+
} from "../platforms.js";
|
|
14
|
+
import { platformInstallClassName as cx } from "./platform-install.stylex.js";
|
|
15
|
+
|
|
16
|
+
/** Another way to install on the same platform, such as npm or Homebrew. */
|
|
17
|
+
export interface PlatformInstallAlternative {
|
|
18
|
+
/** Names the route, such as "npm" or "Homebrew". */
|
|
19
|
+
readonly label: string;
|
|
20
|
+
readonly command: string;
|
|
21
|
+
/** Where the command runs, such as "Terminal" or "PowerShell". */
|
|
22
|
+
readonly shell?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PlatformInstallTargetBase {
|
|
26
|
+
readonly id: PlatformId;
|
|
27
|
+
/** Required for ids other than `macos`, `linux`, and `windows`, which default to their display names. */
|
|
28
|
+
readonly label?: string;
|
|
29
|
+
/** Where the command runs, such as "Terminal" or "PowerShell". */
|
|
30
|
+
readonly shell?: string;
|
|
31
|
+
/** A short qualifier under the command, such as "Apple silicon" or "x86_64 and ARM64, glibc 2.34+". */
|
|
32
|
+
readonly note?: ReactNode;
|
|
33
|
+
readonly alternatives?: readonly PlatformInstallAlternative[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A platform with a native install command. */
|
|
37
|
+
export interface AvailablePlatformInstallTarget extends PlatformInstallTargetBase {
|
|
38
|
+
readonly command: string;
|
|
39
|
+
readonly unavailable?: false;
|
|
40
|
+
readonly unavailableNote?: never;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A platform without a native build. The note says what to do instead
|
|
45
|
+
* ("Build from source", "Runs in WSL2"); a command, when given, is the route
|
|
46
|
+
* that note describes.
|
|
47
|
+
*/
|
|
48
|
+
export interface UnavailablePlatformInstallTarget extends PlatformInstallTargetBase {
|
|
49
|
+
readonly unavailable: true;
|
|
50
|
+
readonly unavailableNote: ReactNode;
|
|
51
|
+
readonly command?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type PlatformInstallTarget = AvailablePlatformInstallTarget | UnavailablePlatformInstallTarget;
|
|
55
|
+
|
|
56
|
+
export interface PlatformInstallProps {
|
|
57
|
+
/** One entry per platform tab, in display order. */
|
|
58
|
+
readonly platforms: readonly PlatformInstallTarget[];
|
|
59
|
+
readonly className?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The platform selected in the server render and before detection.
|
|
62
|
+
* Defaults to the first platform.
|
|
63
|
+
*/
|
|
64
|
+
readonly defaultPlatform?: PlatformId;
|
|
65
|
+
/** Select the visitor's operating system after hydration when it is listed. Defaults to true. */
|
|
66
|
+
readonly detect?: boolean;
|
|
67
|
+
readonly id?: string;
|
|
68
|
+
/** Accessible name for the platform tabs. Defaults to "Platform". */
|
|
69
|
+
readonly label?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type CopyStatus = Readonly<{ key: string; ok: boolean; subject: string }>;
|
|
73
|
+
type SelectionSource = "default" | "detected" | "chosen";
|
|
74
|
+
|
|
75
|
+
const copyResetMilliseconds = 2000;
|
|
76
|
+
|
|
77
|
+
function assertTargets(platforms: readonly PlatformInstallTarget[], defaultPlatform: PlatformId | undefined): void {
|
|
78
|
+
if (platforms.length === 0) throw new RangeError("PlatformInstall needs at least one platform.");
|
|
79
|
+
const seen = new Set<string>();
|
|
80
|
+
for (const target of platforms) {
|
|
81
|
+
if (!isPlatformId(target.id)) throw new RangeError(`Platform ids are lowercase slugs; received ${JSON.stringify(target.id)}.`);
|
|
82
|
+
if (seen.has(target.id)) throw new RangeError(`Duplicate platform id: ${target.id}.`);
|
|
83
|
+
seen.add(target.id);
|
|
84
|
+
if (target.label !== undefined ? target.label.trim() === "" : !isKnownPlatformId(target.id)) {
|
|
85
|
+
throw new RangeError(`Platform ${target.id} needs a label.`);
|
|
86
|
+
}
|
|
87
|
+
if (target.unavailable === true) {
|
|
88
|
+
if (target.unavailableNote === undefined || target.unavailableNote === null || target.unavailableNote === "") {
|
|
89
|
+
throw new RangeError(`Unavailable platform ${target.id} needs an unavailableNote.`);
|
|
90
|
+
}
|
|
91
|
+
if (target.command !== undefined && target.command.trim() === "") throw new RangeError(`Platform ${target.id} has a blank command.`);
|
|
92
|
+
} else {
|
|
93
|
+
if (typeof target.command !== "string" || target.command.trim() === "") throw new RangeError(`Platform ${target.id} needs a command.`);
|
|
94
|
+
}
|
|
95
|
+
for (const alternative of target.alternatives ?? []) {
|
|
96
|
+
if (alternative.label.trim() === "" || alternative.command.trim() === "") {
|
|
97
|
+
throw new RangeError(`Platform ${target.id} has an alternative without a label or command.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (defaultPlatform !== undefined && !seen.has(defaultPlatform)) {
|
|
102
|
+
throw new RangeError(`defaultPlatform ${JSON.stringify(defaultPlatform)} is not one of the listed platforms.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function labelOf(target: PlatformInstallTarget): string {
|
|
107
|
+
return target.label ?? platformLabel(target.id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function selectContents(element: HTMLElement | null): void {
|
|
111
|
+
if (element === null) return;
|
|
112
|
+
try {
|
|
113
|
+
const selection = element.ownerDocument.defaultView?.getSelection();
|
|
114
|
+
if (selection === null || selection === undefined) return;
|
|
115
|
+
const range = element.ownerDocument.createRange();
|
|
116
|
+
range.selectNodeContents(element);
|
|
117
|
+
selection.removeAllRanges();
|
|
118
|
+
selection.addRange(range);
|
|
119
|
+
} catch {
|
|
120
|
+
// Selection is a convenience; the failure is still announced.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function writeClipboard(text: string, fallback: HTMLElement | null): Promise<boolean> {
|
|
125
|
+
try {
|
|
126
|
+
if (typeof navigator !== "undefined" && navigator.clipboard !== undefined) {
|
|
127
|
+
await navigator.clipboard.writeText(text);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Fall through to the selection route below.
|
|
132
|
+
}
|
|
133
|
+
// Clipboard access is blocked or unavailable: select the command so the
|
|
134
|
+
// visitor can copy it by hand, and try the legacy copy command once.
|
|
135
|
+
selectContents(fallback);
|
|
136
|
+
try {
|
|
137
|
+
return fallback?.ownerDocument.execCommand("copy") === true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Each listed platform's mark is defined once per component as a `<symbol>`
|
|
145
|
+
* and drawn by reference, so the tab and the no-script panel label do not
|
|
146
|
+
* repeat the full path (the Tux mark alone is several kilobytes).
|
|
147
|
+
*/
|
|
148
|
+
function PlatformMarkSymbols({ ids, symbolId }: Readonly<{ ids: readonly PlatformId[]; symbolId: (id: PlatformId) => string }>) {
|
|
149
|
+
return (
|
|
150
|
+
<svg aria-hidden="true" className={cx(["markSymbols"])} focusable="false" xmlns="http://www.w3.org/2000/svg">
|
|
151
|
+
{ids.map((id) => {
|
|
152
|
+
const mark = platformMark(id);
|
|
153
|
+
return <symbol id={symbolId(id)} key={id} viewBox={mark.viewBox}><path d={mark.path} /></symbol>;
|
|
154
|
+
})}
|
|
155
|
+
</svg>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The decorative PlatformIcon, drawn from the component's shared symbol. */
|
|
160
|
+
function PlatformMarkUse({ platform, symbolId }: Readonly<{ platform: PlatformId; symbolId: string }>) {
|
|
161
|
+
return (
|
|
162
|
+
<svg aria-hidden="true" className={cx(["icon"])} data-platform={platform} fill="currentColor" focusable="false"
|
|
163
|
+
viewBox={platformMark(platform).viewBox} xmlns="http://www.w3.org/2000/svg">
|
|
164
|
+
<use href={`#${symbolId}`} />
|
|
165
|
+
</svg>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function CopyGlyph({ copied }: Readonly<{ copied: boolean }>) {
|
|
170
|
+
return (
|
|
171
|
+
<svg aria-hidden="true" className={cx(["copyIcon"])} fill="none" focusable="false" stroke="currentColor"
|
|
172
|
+
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} viewBox="0 0 24 24">
|
|
173
|
+
{copied
|
|
174
|
+
? <path d="M5 12.5l4.5 4.5L19 7.5" />
|
|
175
|
+
: <><rect height="12" rx="2" width="12" x="8" y="8" /><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" /></>}
|
|
176
|
+
</svg>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function CommandBlock({
|
|
181
|
+
caption,
|
|
182
|
+
command,
|
|
183
|
+
copyKey,
|
|
184
|
+
onCopy,
|
|
185
|
+
status,
|
|
186
|
+
subject,
|
|
187
|
+
}: Readonly<{
|
|
188
|
+
caption: string | undefined;
|
|
189
|
+
command: string;
|
|
190
|
+
copyKey: string;
|
|
191
|
+
onCopy: (key: string, command: string, subject: string, pre: HTMLElement | null) => void;
|
|
192
|
+
status: CopyStatus | null;
|
|
193
|
+
subject: string;
|
|
194
|
+
}>) {
|
|
195
|
+
const preRef = useRef<HTMLPreElement>(null);
|
|
196
|
+
const state = status?.key === copyKey ? (status.ok ? "copied" : "failed") : "idle";
|
|
197
|
+
return (
|
|
198
|
+
<div className={cx(["command"])} data-copy-state={state}>
|
|
199
|
+
<div className={cx(["commandBar", (caption === undefined || caption === "") && "commandBarEmpty"])}>
|
|
200
|
+
<span className={cx(["shell"])}>{caption}</span>
|
|
201
|
+
<button
|
|
202
|
+
className={cx(["copy"])}
|
|
203
|
+
data-copy-state={state}
|
|
204
|
+
onClick={() => onCopy(copyKey, command, subject, preRef.current)}
|
|
205
|
+
type="button"
|
|
206
|
+
>
|
|
207
|
+
<CopyGlyph copied={state === "copied"} />
|
|
208
|
+
<span>{state === "copied" ? "Copied" : state === "failed" ? "Select to copy" : "Copy"}</span>
|
|
209
|
+
<span className={cx(["status"])}> {subject}</span>
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
{/* Focusable so keyboard users can scroll a long command sideways. */}
|
|
213
|
+
<pre aria-label={subject} className={cx(["pre"])} ref={preRef} tabIndex={0}>
|
|
214
|
+
<code className={cx(["code"])}>{command}</code>
|
|
215
|
+
</pre>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Install commands for each supported platform behind one tab row with
|
|
222
|
+
* platform icons. The server render selects `defaultPlatform` (or the first
|
|
223
|
+
* platform); after hydration the visitor's operating system is selected when
|
|
224
|
+
* it is listed. Each command has a copy button that announces the result.
|
|
225
|
+
* Without JavaScript the tabs and copy buttons hide and every platform's
|
|
226
|
+
* commands show in sequence.
|
|
227
|
+
*/
|
|
228
|
+
export function PlatformInstall({
|
|
229
|
+
className,
|
|
230
|
+
defaultPlatform,
|
|
231
|
+
detect = true,
|
|
232
|
+
id,
|
|
233
|
+
label = "Platform",
|
|
234
|
+
platforms,
|
|
235
|
+
}: PlatformInstallProps) {
|
|
236
|
+
assertTargets(platforms, defaultPlatform);
|
|
237
|
+
const [firstTarget] = platforms;
|
|
238
|
+
if (firstTarget === undefined) throw new RangeError("PlatformInstall needs at least one platform.");
|
|
239
|
+
const generatedId = useId();
|
|
240
|
+
const baseId = id ?? `hraness-platform-install${generatedId.replaceAll(/[^A-Za-z0-9_-]/gu, "")}`;
|
|
241
|
+
const initial = defaultPlatform ?? firstTarget.id;
|
|
242
|
+
const [selected, setSelected] = useState<PlatformId>(initial);
|
|
243
|
+
const [source, setSource] = useState<SelectionSource>("default");
|
|
244
|
+
const [status, setStatus] = useState<CopyStatus | null>(null);
|
|
245
|
+
const chosen = useRef(false);
|
|
246
|
+
const tabs = useRef(new Map<string, HTMLButtonElement>());
|
|
247
|
+
const ids = platforms.map((target) => target.id);
|
|
248
|
+
const idKey = ids.join("\n");
|
|
249
|
+
const symbolId = (platform: PlatformId) => `${baseId}-mark-${platform}`;
|
|
250
|
+
|
|
251
|
+
// A removed platform falls back to the first listed one.
|
|
252
|
+
const current = ids.includes(selected) ? selected : initial;
|
|
253
|
+
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
if (!detect || chosen.current) return;
|
|
256
|
+
const match = matchDetectedPlatform(detectPlatform(globalThis.navigator), idKey.split("\n"));
|
|
257
|
+
if (match === null) return;
|
|
258
|
+
setSelected(match);
|
|
259
|
+
setSource("detected");
|
|
260
|
+
}, [detect, idKey]);
|
|
261
|
+
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
if (status === null) return;
|
|
264
|
+
const timer = setTimeout(() => setStatus(null), copyResetMilliseconds);
|
|
265
|
+
return () => clearTimeout(timer);
|
|
266
|
+
}, [status]);
|
|
267
|
+
|
|
268
|
+
const choose = useCallback((next: PlatformId, focus: boolean) => {
|
|
269
|
+
chosen.current = true;
|
|
270
|
+
setSelected(next);
|
|
271
|
+
setSource("chosen");
|
|
272
|
+
if (focus) tabs.current.get(next)?.focus();
|
|
273
|
+
}, []);
|
|
274
|
+
|
|
275
|
+
const copy = useCallback((key: string, command: string, subject: string, pre: HTMLElement | null) => {
|
|
276
|
+
void writeClipboard(command, pre).then((ok) => setStatus({ key, ok, subject }));
|
|
277
|
+
}, []);
|
|
278
|
+
|
|
279
|
+
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
280
|
+
const index = ids.indexOf(current);
|
|
281
|
+
let next: number;
|
|
282
|
+
switch (event.key) {
|
|
283
|
+
case "ArrowRight": next = (index + 1) % ids.length; break;
|
|
284
|
+
case "ArrowLeft": next = (index - 1 + ids.length) % ids.length; break;
|
|
285
|
+
case "Home": next = 0; break;
|
|
286
|
+
case "End": next = ids.length - 1; break;
|
|
287
|
+
default: return;
|
|
288
|
+
}
|
|
289
|
+
const target = ids[next];
|
|
290
|
+
if (target === undefined) return;
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
choose(target, true);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<div
|
|
297
|
+
className={cx(["root"], className)}
|
|
298
|
+
data-hraness-platform-install=""
|
|
299
|
+
data-selected-platform={current}
|
|
300
|
+
data-selection-source={source}
|
|
301
|
+
id={id}
|
|
302
|
+
>
|
|
303
|
+
<PlatformMarkSymbols ids={ids} symbolId={symbolId} />
|
|
304
|
+
<div aria-label={label} className={cx(["tablist"])} onKeyDown={onKeyDown} role="tablist">
|
|
305
|
+
{platforms.map((target) => {
|
|
306
|
+
const isSelected = target.id === current;
|
|
307
|
+
return (
|
|
308
|
+
<button
|
|
309
|
+
aria-controls={`${baseId}-panel-${target.id}`}
|
|
310
|
+
aria-selected={isSelected}
|
|
311
|
+
className={cx(["tab", isSelected && "tabSelected"])}
|
|
312
|
+
data-availability={target.unavailable === true ? "unavailable" : "available"}
|
|
313
|
+
data-platform={target.id}
|
|
314
|
+
id={`${baseId}-tab-${target.id}`}
|
|
315
|
+
key={target.id}
|
|
316
|
+
onClick={() => choose(target.id, false)}
|
|
317
|
+
ref={(element) => {
|
|
318
|
+
if (element === null) tabs.current.delete(target.id);
|
|
319
|
+
else tabs.current.set(target.id, element);
|
|
320
|
+
}}
|
|
321
|
+
role="tab"
|
|
322
|
+
tabIndex={isSelected ? 0 : -1}
|
|
323
|
+
type="button"
|
|
324
|
+
>
|
|
325
|
+
<PlatformMarkUse platform={target.id} symbolId={symbolId(target.id)} />
|
|
326
|
+
<span className={cx(["tabLabel"])}>{labelOf(target)}</span>
|
|
327
|
+
</button>
|
|
328
|
+
);
|
|
329
|
+
})}
|
|
330
|
+
</div>
|
|
331
|
+
{platforms.map((target) => {
|
|
332
|
+
const name = labelOf(target);
|
|
333
|
+
const alternatives = target.alternatives ?? [];
|
|
334
|
+
return (
|
|
335
|
+
<div
|
|
336
|
+
aria-labelledby={`${baseId}-tab-${target.id}`}
|
|
337
|
+
className={cx(["panel"])}
|
|
338
|
+
data-availability={target.unavailable === true ? "unavailable" : "available"}
|
|
339
|
+
data-platform={target.id}
|
|
340
|
+
hidden={target.id !== current}
|
|
341
|
+
id={`${baseId}-panel-${target.id}`}
|
|
342
|
+
key={target.id}
|
|
343
|
+
role="tabpanel"
|
|
344
|
+
>
|
|
345
|
+
<div className={cx(["panelBody"])}>
|
|
346
|
+
<p className={cx(["panelLabel"])}>
|
|
347
|
+
<PlatformMarkUse platform={target.id} symbolId={symbolId(target.id)} />
|
|
348
|
+
<span>{name}</span>
|
|
349
|
+
</p>
|
|
350
|
+
{target.unavailable === true
|
|
351
|
+
? <div className={cx(["unavailable"])}>{target.unavailableNote}</div>
|
|
352
|
+
: null}
|
|
353
|
+
{target.command === undefined ? null : (
|
|
354
|
+
<CommandBlock
|
|
355
|
+
caption={target.shell}
|
|
356
|
+
command={target.command}
|
|
357
|
+
copyKey={`${target.id}:primary`}
|
|
358
|
+
onCopy={copy}
|
|
359
|
+
status={status}
|
|
360
|
+
subject={`${name} install command`}
|
|
361
|
+
/>
|
|
362
|
+
)}
|
|
363
|
+
{alternatives.length === 0 ? null : (
|
|
364
|
+
<ul aria-label={`Other ways to install on ${name}`} className={cx(["alternatives"])}>
|
|
365
|
+
{alternatives.map((alternative, index) => (
|
|
366
|
+
<li key={`${alternative.label}-${index}`}>
|
|
367
|
+
<CommandBlock
|
|
368
|
+
caption={[alternative.label, alternative.shell].filter(Boolean).join(" · ")}
|
|
369
|
+
command={alternative.command}
|
|
370
|
+
copyKey={`${target.id}:${index}`}
|
|
371
|
+
onCopy={copy}
|
|
372
|
+
status={status}
|
|
373
|
+
subject={`${name} ${alternative.label} command`}
|
|
374
|
+
/>
|
|
375
|
+
</li>
|
|
376
|
+
))}
|
|
377
|
+
</ul>
|
|
378
|
+
)}
|
|
379
|
+
{target.note === undefined ? null : <div className={cx(["note"])}>{target.note}</div>}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
})}
|
|
384
|
+
<p aria-live="polite" className={cx(["status"])} role="status">
|
|
385
|
+
{status === null ? "" : status.ok ? `Copied the ${status.subject}.` : `Copying failed. The ${status.subject} is selected; copy it with your keyboard.`}
|
|
386
|
+
</p>
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as stylex from "@stylexjs/stylex";
|
|
2
|
+
|
|
3
|
+
export const playbackTransportStyles = stylex.create({
|
|
4
|
+
glyph: {
|
|
5
|
+
"block-size": "1.5rem",
|
|
6
|
+
"inline-size": "1.5rem",
|
|
7
|
+
},
|
|
8
|
+
root: {
|
|
9
|
+
alignItems: "center",
|
|
10
|
+
display: "flex",
|
|
11
|
+
flexWrap: "wrap",
|
|
12
|
+
gap: "var(--space-2)",
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PlayIcon,
|
|
5
|
+
StopIcon,
|
|
6
|
+
} from "@hugeicons/core-free-icons";
|
|
7
|
+
import { Icon, IconButton, Spinner, Toolbar, cn } from "@hraness/ui";
|
|
8
|
+
import * as stylex from "@stylexjs/stylex";
|
|
9
|
+
import type { ReactNode, Ref } from "react";
|
|
10
|
+
|
|
11
|
+
import { playbackTransportStyles } from "./playback-transport.stylex.js";
|
|
12
|
+
|
|
13
|
+
type AccessibleName =
|
|
14
|
+
| { readonly "aria-label": string; readonly "aria-labelledby"?: never }
|
|
15
|
+
| { readonly "aria-label"?: never; readonly "aria-labelledby": string };
|
|
16
|
+
|
|
17
|
+
export type PlaybackTransportStatus = "idle" | "pending" | "playing";
|
|
18
|
+
|
|
19
|
+
export type PlaybackTransportProps = AccessibleName & {
|
|
20
|
+
readonly buttonAriaKeyShortcuts?: string;
|
|
21
|
+
readonly buttonId?: string;
|
|
22
|
+
readonly buttonRef?: Ref<HTMLButtonElement>;
|
|
23
|
+
readonly className?: string;
|
|
24
|
+
readonly isPlayDisabled?: boolean;
|
|
25
|
+
readonly onPlay: () => void;
|
|
26
|
+
readonly onStop: () => void;
|
|
27
|
+
readonly pendingLabel?: string;
|
|
28
|
+
readonly playLabel?: string;
|
|
29
|
+
readonly status: PlaybackTransportStatus;
|
|
30
|
+
readonly stopLabel?: string;
|
|
31
|
+
readonly trailingControls?: ReactNode;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One stable, icon-only playback command for product-owned audio behavior.
|
|
36
|
+
*
|
|
37
|
+
* The same touch-sized node starts idle playback, cancels pending startup, and
|
|
38
|
+
* stops active playback. Its accessible name and glyph describe the current
|
|
39
|
+
* command without adding a persistent visual label to compact transport chrome.
|
|
40
|
+
*/
|
|
41
|
+
export function PlaybackTransport({
|
|
42
|
+
buttonAriaKeyShortcuts,
|
|
43
|
+
buttonId,
|
|
44
|
+
buttonRef,
|
|
45
|
+
className,
|
|
46
|
+
isPlayDisabled = false,
|
|
47
|
+
onPlay,
|
|
48
|
+
onStop,
|
|
49
|
+
pendingLabel = "Cancel playback start",
|
|
50
|
+
playLabel = "Play",
|
|
51
|
+
status,
|
|
52
|
+
stopLabel = "Stop",
|
|
53
|
+
trailingControls,
|
|
54
|
+
...accessibleName
|
|
55
|
+
}: PlaybackTransportProps) {
|
|
56
|
+
const isPending = status === "pending";
|
|
57
|
+
const isIdle = status === "idle";
|
|
58
|
+
const commandLabel = isIdle
|
|
59
|
+
? playLabel
|
|
60
|
+
: isPending
|
|
61
|
+
? pendingLabel
|
|
62
|
+
: stopLabel;
|
|
63
|
+
const rootPresentation = stylex.props(playbackTransportStyles.root);
|
|
64
|
+
const glyphPresentation = stylex.props(playbackTransportStyles.glyph);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Toolbar
|
|
68
|
+
{...accessibleName}
|
|
69
|
+
className={cn(
|
|
70
|
+
"hraness-design-playback-transport",
|
|
71
|
+
rootPresentation.className,
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
data-playback-status={status}
|
|
75
|
+
>
|
|
76
|
+
<IconButton
|
|
77
|
+
aria-busy={isPending || undefined}
|
|
78
|
+
aria-label={commandLabel}
|
|
79
|
+
{...(buttonAriaKeyShortcuts === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { "aria-keyshortcuts": buttonAriaKeyShortcuts })}
|
|
82
|
+
{...(buttonId === undefined ? {} : { id: buttonId })}
|
|
83
|
+
{...(buttonRef === undefined ? {} : { buttonRef })}
|
|
84
|
+
className="hraness-design-playback-transport__button"
|
|
85
|
+
data-playback-command={isIdle ? "play" : "stop"}
|
|
86
|
+
isDisabled={isIdle && isPlayDisabled}
|
|
87
|
+
onPress={() => {
|
|
88
|
+
if (isIdle) {
|
|
89
|
+
if (!isPlayDisabled) onPlay();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
onStop();
|
|
93
|
+
}}
|
|
94
|
+
size="large"
|
|
95
|
+
variant="primary"
|
|
96
|
+
>
|
|
97
|
+
{isPending
|
|
98
|
+
? <Spinner {...glyphPresentation} />
|
|
99
|
+
: (
|
|
100
|
+
<Icon
|
|
101
|
+
{...(glyphPresentation.className === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: { className: glyphPresentation.className })}
|
|
104
|
+
icon={isIdle ? PlayIcon : StopIcon}
|
|
105
|
+
size={24}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
</IconButton>
|
|
109
|
+
{trailingControls}
|
|
110
|
+
</Toolbar>
|
|
111
|
+
);
|
|
112
|
+
}
|