diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/examples-list.json b/examples-list.json index ba3e9f1e..4ae191d2 100644 --- a/examples-list.json +++ b/examples-list.json @@ -1,4 +1,8 @@ [ + { + "path": "components-overview", + "title": "Components overview" + }, { "path": "cards", "title": "Card view" @@ -7,10 +11,6 @@ "path": "chat", "title": "Chat" }, - { - "path": "delete-one-click", - "title": "One-click delete" - }, { "path": "delete-with-simple-confirmation", "title": "Delete with simple confirmation" @@ -51,14 +51,6 @@ "path": "product-detail-page", "title": "Product detail page" }, - { - "path": "server-side-table", - "title": "Table view (server-side)" - }, - { - "path": "server-side-table-property-filter", - "title": "Table property filter (server-side)" - }, { "path": "table-property-filter", "title": "Table property filter" @@ -91,14 +83,6 @@ "path": "manage-tags", "title": "Manage tags" }, - { - "path": "read-from-s3", - "title": "Read from Amazon S3" - }, - { - "path": "write-to-s3", - "title": "Write to Amazon S3" - }, { "path": "wizard", "title": "Multipage create" diff --git a/package.json b/package.json index 41d24f80..c593808f 100644 --- a/package.json +++ b/package.json @@ -26,14 +26,14 @@ "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", - "@cloudscape-design/board-components": "^3.0.0", + "@cloudscape-design/board-components": "^3.0.150", "@cloudscape-design/browser-test-tools": "^3.0.0", - "@cloudscape-design/chart-components": "^1.0.0", - "@cloudscape-design/chat-components": "^1.0.0", - "@cloudscape-design/code-view": "^3.0.0", + "@cloudscape-design/chart-components": "^1.0.57", + "@cloudscape-design/chat-components": "^1.0.99", + "@cloudscape-design/code-view": "^3.0.104", "@cloudscape-design/collection-hooks": "^1.0.0", "@cloudscape-design/component-toolkit": "^1.0.0-beta", - "@cloudscape-design/components": "^3.0.0", + "@cloudscape-design/components": "^3.0.1217", "@cloudscape-design/design-tokens": "^3.0.0", "@cloudscape-design/global-styles": "^1.0.0", "@cloudscape-design/test-utils-core": "^1.0.0", diff --git a/scripts/add-global-drawer.js b/scripts/add-global-drawer.js new file mode 100644 index 00000000..76ace431 --- /dev/null +++ b/scripts/add-global-drawer.js @@ -0,0 +1,110 @@ +#!/usr/bin/env node +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +// Script to add WithGlobalDrawer wrapper to all page index files + +import fs from 'fs'; +import path from 'path'; +import { glob } from 'glob'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const pagesDir = path.join(__dirname, '../src/pages'); + +// Pages that already have the wrapper or have special handling +const skipPages = ['cards', 'components-overview', 'commons']; + +function updateIndexFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + + // Skip if already has WithGlobalDrawer + if (content.includes('WithGlobalDrawer')) { + console.log(`Skipping ${filePath} - already updated`); + return false; + } + + let updated = content; + + // Add WithGlobalDrawer to imports from common-components + if (content.includes("from '../commons/common-components'")) { + updated = updated.replace(/(from ['"]\.\.\/commons\/common-components['"])/, match => { + const importLine = content.match(/import\s+{([^}]+)}\s+from\s+['"]\.\.\/commons\/common-components['"]/); + if (importLine && !importLine[1].includes('WithGlobalDrawer')) { + return match.replace(/import\s+{([^}]+)}/, (m, imports) => `import { ${imports.trim()}, WithGlobalDrawer }`); + } + return match; + }); + } else if (content.includes("from '../commons'")) { + // Add import if only importing from commons + updated = updated.replace( + /(import\s+{[^}]+}\s+from\s+['"]\.\.\/commons['"];)/, + `$1\nimport { WithGlobalDrawer } from '../commons/common-components';`, + ); + } else { + // Add new import line + const lastImportIndex = updated.lastIndexOf('import '); + const endOfLine = updated.indexOf('\n', lastImportIndex); + updated = + updated.slice(0, endOfLine + 1) + + "import { WithGlobalDrawer } from '../commons/common-components';\n" + + updated.slice(endOfLine + 1); + } + + // Wrap the render call with WithGlobalDrawer + // Handle simple case: render() + updated = updated.replace( + /createRoot\(document\.getElementById\(['"]app['"]\)!\)\.render\(\);/, + `createRoot(document.getElementById('app')!).render(\n \n \n \n);`, + ); + + // Handle case with props: render() + updated = updated.replace( + /createRoot\(document\.getElementById\(['"]app['"]\)!\)\.render\(]+)\/>\);/, + `createRoot(document.getElementById('app')!).render(\n \n \n \n);`, + ); + + // Handle multiline render with existing wrapper (like onboarding) + updated = updated.replace( + /createRoot\(document\.getElementById\(['"]app['"]\)!\)\.render\(\s*\n\s*<(\w+)>\s*\n\s*/, + (match, wrapper) => { + if (wrapper !== 'WithGlobalDrawer') { + return `createRoot(document.getElementById('app')!).render(\n \n <${wrapper}>\n `; + } + return match; + }, + ); + + if (updated !== content) { + fs.writeFileSync(filePath, updated, 'utf8'); + console.log(`Updated ${filePath}`); + return true; + } + + console.log(`No changes needed for ${filePath}`); + return false; +} + +async function main() { + const indexFiles = await glob('**/index.tsx', { cwd: pagesDir, absolute: true }); + + let updatedCount = 0; + for (const file of indexFiles) { + const pageName = path.basename(path.dirname(file)); + if (skipPages.includes(pageName)) { + console.log(`Skipping ${pageName} (in skip list)`); + continue; + } + + const wasUpdated = updateIndexFile(file); + if (wasUpdated) { + updatedCount++; + } + } + + console.log(`\nCompleted! Updated ${updatedCount} files.`); +} + +main().catch(console.error); diff --git a/scripts/add-split-panel-preferences.js b/scripts/add-split-panel-preferences.js new file mode 100755 index 00000000..77c9699c --- /dev/null +++ b/scripts/add-split-panel-preferences.js @@ -0,0 +1,92 @@ +#!/usr/bin/env node +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const pagesDir = path.join(__dirname, '../src/pages'); + +function addSplitPanelPreferences(filePath) { + let content = fs.readFileSync(filePath, 'utf8'); + + // Check if file uses useGlobalSplitPanel + if (!content.includes('useGlobalSplitPanel')) { + return false; + } + + // Check if splitPanelPreferences is already in the file + if (content.includes('splitPanelPreferences')) { + // Check if it's in the destructuring + const hookPattern = /const\s+{\s*([^}]+)\s*}\s*=\s*useGlobalSplitPanel\(\)/; + const match = content.match(hookPattern); + + if (match && !match[1].includes('splitPanelPreferences')) { + // Add to destructuring + const currentProps = match[1]; + const newProps = currentProps.trim() + ',\n splitPanelPreferences'; + content = content.replace(hookPattern, `const { ${newProps} } = useGlobalSplitPanel()`); + } + + // Check if prop is added to CustomAppLayout + if (!content.includes('splitPanelPreferences={splitPanelPreferences}')) { + // Add prop before splitPanel= + const pattern = /(onSplitPanelResize=\{onSplitPanelResize\})\s*\n(\s*)(splitPanel=)/; + if (pattern.test(content)) { + content = content.replace(pattern, '$1\n$2splitPanelPreferences={splitPanelPreferences}\n$2$3'); + fs.writeFileSync(filePath, content, 'utf8'); + return true; + } + } + return false; + } + + // Add splitPanelPreferences to destructuring + const hookPattern = /const\s+{\s*([^}]+)\s*}\s*=\s*useGlobalSplitPanel\(\)/; + const match = content.match(hookPattern); + + if (match) { + const currentProps = match[1]; + const newProps = currentProps.trim() + ',\n splitPanelPreferences'; + content = content.replace(hookPattern, `const { ${newProps} } = useGlobalSplitPanel()`); + + // Add prop before splitPanel= + const pattern = /(onSplitPanelResize=\{onSplitPanelResize\})\s*\n(\s*)(splitPanel=)/; + if (pattern.test(content)) { + content = content.replace(pattern, '$1\n$2splitPanelPreferences={splitPanelPreferences}\n$2$3'); + } + + fs.writeFileSync(filePath, content, 'utf8'); + return true; + } + + return false; +} + +function processDirectory(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + let updatedCount = 0; + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + updatedCount += processDirectory(fullPath); + } else if (entry.name === 'app.tsx' || entry.name === 'root.tsx') { + if (addSplitPanelPreferences(fullPath)) { + console.log(`✓ Updated: ${path.relative(pagesDir, fullPath)}`); + updatedCount++; + } + } + } + + return updatedCount; +} + +console.log('Adding splitPanelPreferences prop to CustomAppLayout...\n'); +const count = processDirectory(pagesDir); +console.log(`\n✓ Updated ${count} files`); diff --git a/scripts/cleanup-old-drawer.sh b/scripts/cleanup-old-drawer.sh new file mode 100755 index 00000000..e1c6267d --- /dev/null +++ b/scripts/cleanup-old-drawer.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Cleanup script to remove old global drawer files after migration + +echo "🧹 Cleaning up old global drawer files..." + +# Remove old drawer implementation files +if [ -f "src/pages/commons/global-drawer-plugin.tsx" ]; then + rm src/pages/commons/global-drawer-plugin.tsx + echo "✓ Removed src/pages/commons/global-drawer-plugin.tsx" +fi + +if [ -f "src/pages/commons/with-global-drawer.tsx" ]; then + rm src/pages/commons/with-global-drawer.tsx + echo "✓ Removed src/pages/commons/with-global-drawer.tsx" +fi + +if [ -f "src/common/mount.tsx" ]; then + rm src/common/mount.tsx + echo "✓ Removed src/common/mount.tsx" +fi + +if [ -f "scripts/add-global-drawer.js" ]; then + rm scripts/add-global-drawer.js + echo "✓ Removed scripts/add-global-drawer.js" +fi + +if [ -f "GLOBAL_DRAWER_IMPLEMENTATION.md" ]; then + rm GLOBAL_DRAWER_IMPLEMENTATION.md + echo "✓ Removed GLOBAL_DRAWER_IMPLEMENTATION.md" +fi + +echo "" +echo "✅ Cleanup complete!" +echo "" +echo "Next steps:" +echo "1. Update src/pages/commons/common-components.tsx to remove old exports" +echo "2. Run 'npm run build' to verify everything still works" +echo "3. Test the pages in the browser" diff --git a/scripts/fix-split-panel-props.js b/scripts/fix-split-panel-props.js new file mode 100755 index 00000000..ba866646 --- /dev/null +++ b/scripts/fix-split-panel-props.js @@ -0,0 +1,105 @@ +#!/usr/bin/env node +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +/** + * Fix script to add missing split panel hook calls and props + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const pagesDir = path.join(__dirname, '../src/pages'); +const skipPages = ['commons', 'split-panel-comparison', 'split-panel-multiple']; + +function addSplitPanelToFile(filePath) { + let content = fs.readFileSync(filePath, 'utf8'); + + // Check if already has split panel props + if (content.includes('splitPanelOpen=') || content.includes('splitPanel=')) { + console.log(`⊘ Skipped ${filePath} (already has split panel)`); + return false; + } + + // Check if has useGlobalSplitPanel import + if (!content.includes('useGlobalSplitPanel')) { + console.log(`⊘ Skipped ${filePath} (no useGlobalSplitPanel import)`); + return false; + } + + // Add hook call after toolsOpen state + const hookCall = + ' const { splitPanelOpen, onSplitPanelToggle, splitPanelSize, onSplitPanelResize } = useGlobalSplitPanel();'; + + // Try to add after toolsOpen declaration + if (content.includes('const [toolsOpen, setToolsOpen]')) { + content = content.replace(/(const \[toolsOpen, setToolsOpen\][^\n]*\n)/, `$1${hookCall}\n`); + } else if (content.includes('useState(false);') && content.includes('toolsOpen')) { + // Alternative pattern + content = content.replace(/(const \[toolsOpen[^\n]*\n)/, `$1${hookCall}\n`); + } + + // Add split panel props before content prop in CustomAppLayout + const splitPanelProps = ` splitPanelOpen={splitPanelOpen} + onSplitPanelToggle={onSplitPanelToggle} + splitPanelSize={splitPanelSize} + onSplitPanelResize={onSplitPanelResize} + splitPanel={ + + + + } +`; + + // Find CustomAppLayout and add props before content= + content = content.replace(/(\s+)(content=\{)/, `$1${splitPanelProps}$1$2`); + + fs.writeFileSync(filePath, content, 'utf8'); + return true; +} + +function processPage(pageName) { + const pageDir = path.join(pagesDir, pageName); + const rootPath = path.join(pageDir, 'root.tsx'); + const appPath = path.join(pageDir, 'app.tsx'); + + let fixed = false; + + try { + if (fs.existsSync(rootPath)) { + if (addSplitPanelToFile(rootPath)) { + console.log(`✓ Fixed ${rootPath}`); + fixed = true; + } + } else if (fs.existsSync(appPath)) { + if (addSplitPanelToFile(appPath)) { + console.log(`✓ Fixed ${appPath}`); + fixed = true; + } + } + } catch (error) { + console.error(`✗ Error fixing ${pageName}:`, error.message); + } + + return fixed; +} + +const pages = fs.readdirSync(pagesDir).filter(name => { + const stat = fs.statSync(path.join(pagesDir, name)); + return stat.isDirectory() && !skipPages.includes(name); +}); + +console.log(`Fixing ${pages.length} pages...\n`); + +let fixedCount = 0; +pages.forEach(pageName => { + if (processPage(pageName)) { + fixedCount++; + } +}); + +console.log(`\n✓ Fixed ${fixedCount} pages!`); diff --git a/scripts/generate-html-files.js b/scripts/generate-html-files.js index 0cb92ee3..38bb63a4 100644 --- a/scripts/generate-html-files.js +++ b/scripts/generate-html-files.js @@ -40,7 +40,7 @@ function getPageContent(pageName, { title }) {
-
diff --git a/scripts/migrate-to-split-panel.js b/scripts/migrate-to-split-panel.js new file mode 100755 index 00000000..5f1fd773 --- /dev/null +++ b/scripts/migrate-to-split-panel.js @@ -0,0 +1,141 @@ +#!/usr/bin/env node +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +/** + * Migration script to convert pages from global drawer plugin to split panel + * + * This script: + * 1. Removes WithGlobalDrawer wrapper from index.tsx files + * 2. Adds split panel imports and hooks to root.tsx files + * 3. Adds split panel props to CustomAppLayout + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const pagesDir = path.join(__dirname, '../src/pages'); + +// Pages to skip (already migrated or special cases) +const skipPages = ['cards', 'commons', 'split-panel-comparison', 'split-panel-multiple']; + +function migrateIndexFile(indexPath) { + let content = fs.readFileSync(indexPath, 'utf8'); + + // Remove WithGlobalDrawer import + content = content.replace( + /import\s+{\s*WithGlobalDrawer\s*}\s+from\s+['"]\.\.\/commons\/common-components['"];?\n?/g, + '', + ); + + // Remove WithGlobalDrawer wrapper + content = content.replace( + /createRoot\(document\.getElementById\(['"]app['"]\)!\)\.render\(\s*\s*\s*<\/WithGlobalDrawer>\s*\);?/g, + "createRoot(document.getElementById('app')!).render();", + ); + + fs.writeFileSync(indexPath, content, 'utf8'); + console.log(`✓ Migrated ${indexPath}`); +} + +function migrateRootFile(rootPath) { + let content = fs.readFileSync(rootPath, 'utf8'); + + // Check if already has split panel + if (content.includes('useGlobalSplitPanel') || content.includes('splitPanelOpen')) { + console.log(`⊘ Skipped ${rootPath} (already has split panel)`); + return; + } + + // Add SplitPanel import if not present + if (!content.includes('import SplitPanel')) { + content = content.replace( + /(import.*from\s+['"]@cloudscape-design\/components\/app-layout['"];?\n)/, + "$1import SplitPanel from '@cloudscape-design/components/split-panel';\n", + ); + } + + // Add split panel utilities to common-components import + content = content.replace( + /(import\s+{[^}]*)(}\s+from\s+['"]\.\.\/commons\/common-components['"])/, + (match, p1, p2) => { + if (!p1.includes('GlobalSplitPanelContent')) { + return p1 + ',\n GlobalSplitPanelContent,\n useGlobalSplitPanel' + p2; + } + return match; + }, + ); + + // Add hook call in App component + content = content.replace( + /(export\s+(?:function|const)\s+App\s*=\s*\([^)]*\)\s*(?:=>)?\s*{[\s\S]*?)(const\s+\[toolsOpen)/, + '$1const { splitPanelOpen, onSplitPanelToggle, splitPanelSize, onSplitPanelResize } = useGlobalSplitPanel();\n $2', + ); + + // Add split panel props to CustomAppLayout + content = content.replace( + /(onToolsChange=\{[^}]+}\}[\s\n]*)/, + `$1splitPanelOpen={splitPanelOpen} + onSplitPanelToggle={onSplitPanelToggle} + splitPanelSize={splitPanelSize} + onSplitPanelResize={onSplitPanelResize} + splitPanel={ + + + + } + `, + ); + + fs.writeFileSync(rootPath, content, 'utf8'); + console.log(`✓ Migrated ${rootPath}`); +} + +function migratePage(pageName) { + const pageDir = path.join(pagesDir, pageName); + const indexPath = path.join(pageDir, 'index.tsx'); + const rootPath = path.join(pageDir, 'root.tsx'); + const appPath = path.join(pageDir, 'app.tsx'); + + try { + // Migrate index.tsx if it exists + if (fs.existsSync(indexPath)) { + migrateIndexFile(indexPath); + } + + // Migrate root.tsx or app.tsx if they exist + if (fs.existsSync(rootPath)) { + migrateRootFile(rootPath); + } else if (fs.existsSync(appPath)) { + migrateRootFile(appPath); + } + } catch (error) { + console.error(`✗ Error migrating ${pageName}:`, error.message); + } +} + +// Get all page directories +const pages = fs.readdirSync(pagesDir).filter(name => { + const stat = fs.statSync(path.join(pagesDir, name)); + return stat.isDirectory() && !skipPages.includes(name); +}); + +console.log(`Found ${pages.length} pages to migrate\n`); + +pages.forEach(pageName => { + console.log(`\nMigrating ${pageName}...`); + migratePage(pageName); +}); + +console.log('\n✓ Migration complete!'); +console.log('\nNext steps:'); +console.log('1. Review the changes with git diff'); +console.log('2. Test the pages to ensure split panel works correctly'); +console.log('3. Remove the old global drawer files:'); +console.log(' - src/pages/commons/global-drawer-plugin.tsx'); +console.log(' - src/pages/commons/with-global-drawer.tsx'); +console.log(' - src/common/mount.tsx'); diff --git a/src/common/THEMING.md b/src/common/THEMING.md new file mode 100644 index 00000000..891b2024 --- /dev/null +++ b/src/common/THEMING.md @@ -0,0 +1,80 @@ +# Cloudscape Runtime Theming Implementation + +This project implements Cloudscape's runtime theming capability to customize the visual appearance of components. + +## Files + +- `theme-core.ts` - Theme definition with custom design tokens +- `apply-theme.ts` - Utility function to apply the theme (optional helper) + +## Theme Structure + +The theme is defined in `theme-core.ts` and follows Cloudscape's theming API: + +```typescript +export const themeCoreConfig = { + tokens: { + // Global token overrides for light and dark modes + colorBorderButtonNormalDefault: { + light: '#232f3e', + dark: '#e9ebed', + }, + // ... more tokens + }, + contexts: { + // Context-specific overrides + header: { + tokens: { + // Tokens specific to header context + }, + }, + 'app-layout-toolbar': { + // Tokens for app layout toolbar + }, + flashbar: { + // Tokens for flashbar notifications + }, + }, +}; +``` + +## Usage + +The theme is applied in each demo page's `index.tsx` file before rendering: + +```typescript +import { applyTheme } from '@cloudscape-design/components/theming'; +import { themeCoreConfig } from '../../common/theme-core'; + +applyTheme({ theme: themeCoreConfig }); + +createRoot(document.getElementById('app')!).render(); +``` + +## Customization + +To customize the theme: + +1. Edit `src/common/theme-core.ts` +2. Modify token values for light/dark modes +3. Add or remove context-specific overrides +4. Changes will apply to all demo pages + +## Available Contexts + +- `header` - Dark header area (high contrast header variant) +- `top-navigation` - Top navigation component +- `app-layout-toolbar` - App layout toolbar area +- `flashbar` - Flashbar notifications +- `alert` - Alert components + +## Token Categories + +- **Color tokens** - Can have light/dark mode values +- **Border radius tokens** - Applied globally (e.g., `borderRadiusButton`) +- **Typography tokens** - Font families applied globally + +## Resources + +- [Cloudscape Theming Documentation](https://cloudscape.design/foundation/visual-foundation/theming/) +- [Design Tokens Reference](https://cloudscape.design/foundation/visual-foundation/design-tokens/) diff --git a/src/common/apply-mode.ts b/src/common/apply-mode.ts index 7e03b801..b42b7bd1 100644 --- a/src/common/apply-mode.ts +++ b/src/common/apply-mode.ts @@ -1,16 +1,18 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -import { applyDensity, Density, disableMotion } from '@cloudscape-design/global-styles'; +import { applyDensity, applyMode, Density, disableMotion, Mode } from '@cloudscape-design/global-styles'; import * as localStorage from './local-storage'; import '@cloudscape-design/global-styles/index.css'; +import './custom-font.css'; (window as any).disableMotionForTests = disableMotion; // always `true` in this design export const isVisualRefresh = true; +// Initialize density export let currentDensity: Density = localStorage.load('Awsui-Density-Preference') ?? Density.Comfortable; applyDensity(currentDensity); @@ -19,3 +21,23 @@ export function updateDensity(density: string) { localStorage.save('Awsui-Density-Preference', density); currentDensity = density as Density; } + +// Initialize mode +export let currentMode: Mode = localStorage.load('Awsui-Mode-Preference') ?? Mode.Light; +applyMode(currentMode); + +export function updateMode(mode: string) { + applyMode(mode as Mode); + localStorage.save('Awsui-Mode-Preference', mode); + currentMode = mode as Mode; +} + +// Initialize direction +export let currentDirection: string = localStorage.load('Awsui-Direction-Preference') ?? 'ltr'; +document.documentElement.dir = currentDirection; + +export function updateDirection(direction: string) { + document.documentElement.dir = direction; + localStorage.save('Awsui-Direction-Preference', direction); + currentDirection = direction; +} diff --git a/src/common/apply-theme.ts b/src/common/apply-theme.ts new file mode 100644 index 00000000..697bcbd3 --- /dev/null +++ b/src/common/apply-theme.ts @@ -0,0 +1,93 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import { applyTheme } from '@cloudscape-design/components/theming'; + +import { generateThemeConfig, generateThemeConfigConsole, themeCoreConfig } from './theme-core'; + +// Store the reset function from the current theme +let currentThemeReset: (() => void) | null = null; + +/** + * Applies the custom theme to the application + * @param customConfig - Optional custom theme configuration. Pass undefined to reset to defaults. + */ +export function applyCustomTheme(customConfig?: Partial) { + // Always reset previous theme first + if (currentThemeReset) { + currentThemeReset(); + currentThemeReset = null; + } + + // If no config provided, apply empty theme to reset to Cloudscape defaults + if (!customConfig) { + const { reset: resetFn } = applyTheme({ theme: { tokens: {} } }); + currentThemeReset = resetFn; + return; + } + + // Apply the new theme and store its reset function + const { reset: resetFn } = applyTheme({ theme: customConfig as any }); + currentThemeReset = resetFn; +} + +/** + * Resets to Cloudscape defaults (no custom theme) + */ +export function resetToDefaults() { + if (currentThemeReset) { + currentThemeReset(); + currentThemeReset = null; + } + // Apply empty theme to ensure complete reset + const { reset: resetFn } = applyTheme({ theme: { tokens: {} } }); + currentThemeReset = resetFn; +} + +// ============================================================================ +// Theme Comparison API +// ============================================================================ + +/** + * Hook-style API for comparing different theme design directions. + * Ensures complete isolation between themes by resetting before each application. + * + * @example + * ```tsx + * function ThemeComparison() { + * const { applyDirectionA, applyDirectionB, resetToDefault } = useThemeComparison(); + * + * return ( + * + * + * + * + * + * ); + * } + * ``` + */ +export function useThemeComparison() { + const applyDirectionA = (customAccentColor?: { light: string; dark: string }) => { + const themeA = generateThemeConfig(customAccentColor); + applyCustomTheme(themeA); + }; + + const applyDirectionB = () => { + const themeB = generateThemeConfigConsole(); + applyCustomTheme(themeB); + }; + + const resetToDefault = () => { + resetToDefaults(); + }; + + return { + applyDirectionA, + applyDirectionB, + resetToDefault, + }; +} + +// Apply default theme and custom CSS class on module load +applyCustomTheme(); +document.body.classList.add('custom-css-enabled'); diff --git a/src/common/custom-font.css b/src/common/custom-font.css new file mode 100644 index 00000000..3311ec53 --- /dev/null +++ b/src/common/custom-font.css @@ -0,0 +1,52 @@ +/* Ember Modern Text webfont declarations */ +@font-face { + font-family: 'Ember Modern Text'; + src: url('./fonts/EmberModernTextV1.1-Regular.otf') format('opentype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Ember Modern Text'; + src: url('./fonts/EmberModernTextV1.1-Bold.otf') format('opentype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Ember Modern Text'; + src: url('./fonts/EmberModernTextV1.1-Italic.otf') format('opentype'); + font-weight: 400; + font-style: italic; + font-display: swap; +} +@font-face { + font-family: 'Ember Modern Text'; + src: url('./fonts/EmberModernTextV1.1-BoldItalic.otf') format('opentype'); + font-weight: 700; + font-style: italic; + font-display: swap; +} + +/* Noto Sans variable webfont declarations */ +@font-face { + font-family: 'Noto Sans'; + src: url('./fonts/NotoSans-VariableFont_wdth,wght.ttf') format('truetype-variations'); + font-weight: 100 900; + font-stretch: 62.5% 100%; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Noto Sans'; + src: url('./fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf') format('truetype-variations'); + font-weight: 100 900; + font-stretch: 62.5% 100%; + font-style: italic; + font-display: swap; +} + +/* Default font-stretch for Noto Sans */ +:root { + font-stretch: 96%; +} diff --git a/src/common/fonts/AmazonEmberDisplay_Bd.ttf b/src/common/fonts/AmazonEmberDisplay_Bd.ttf new file mode 100644 index 00000000..18d2fec4 Binary files /dev/null and b/src/common/fonts/AmazonEmberDisplay_Bd.ttf differ diff --git a/src/common/fonts/AmazonEmberDisplay_BdIt.ttf b/src/common/fonts/AmazonEmberDisplay_BdIt.ttf new file mode 100644 index 00000000..b9758e17 Binary files /dev/null and b/src/common/fonts/AmazonEmberDisplay_BdIt.ttf differ diff --git a/src/common/fonts/AmazonEmberDisplay_Md.ttf b/src/common/fonts/AmazonEmberDisplay_Md.ttf new file mode 100644 index 00000000..68553254 Binary files /dev/null and b/src/common/fonts/AmazonEmberDisplay_Md.ttf differ diff --git a/src/common/fonts/AmazonEmberDisplay_MdIt.ttf b/src/common/fonts/AmazonEmberDisplay_MdIt.ttf new file mode 100644 index 00000000..f93be278 Binary files /dev/null and b/src/common/fonts/AmazonEmberDisplay_MdIt.ttf differ diff --git a/src/common/fonts/AmazonEmberDisplay_Rg.ttf b/src/common/fonts/AmazonEmberDisplay_Rg.ttf new file mode 100644 index 00000000..b5b38fe0 Binary files /dev/null and b/src/common/fonts/AmazonEmberDisplay_Rg.ttf differ diff --git a/src/common/fonts/AmazonEmberDisplay_RgIt.ttf b/src/common/fonts/AmazonEmberDisplay_RgIt.ttf new file mode 100644 index 00000000..0e01f86b Binary files /dev/null and b/src/common/fonts/AmazonEmberDisplay_RgIt.ttf differ diff --git a/src/common/fonts/EmberModernTextV1.1-Bold.otf b/src/common/fonts/EmberModernTextV1.1-Bold.otf new file mode 100755 index 00000000..e3a4cbd0 Binary files /dev/null and b/src/common/fonts/EmberModernTextV1.1-Bold.otf differ diff --git a/src/common/fonts/EmberModernTextV1.1-BoldItalic.otf b/src/common/fonts/EmberModernTextV1.1-BoldItalic.otf new file mode 100755 index 00000000..9c4968c9 Binary files /dev/null and b/src/common/fonts/EmberModernTextV1.1-BoldItalic.otf differ diff --git a/src/common/fonts/EmberModernTextV1.1-Italic.otf b/src/common/fonts/EmberModernTextV1.1-Italic.otf new file mode 100755 index 00000000..7e0da20e Binary files /dev/null and b/src/common/fonts/EmberModernTextV1.1-Italic.otf differ diff --git a/src/common/fonts/EmberModernTextV1.1-Regular.otf b/src/common/fonts/EmberModernTextV1.1-Regular.otf new file mode 100755 index 00000000..f340064d Binary files /dev/null and b/src/common/fonts/EmberModernTextV1.1-Regular.otf differ diff --git a/src/common/fonts/NotoSans-Bold.ttf b/src/common/fonts/NotoSans-Bold.ttf new file mode 100644 index 00000000..07f0d257 Binary files /dev/null and b/src/common/fonts/NotoSans-Bold.ttf differ diff --git a/src/common/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf b/src/common/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf new file mode 100644 index 00000000..6245ba01 Binary files /dev/null and b/src/common/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf differ diff --git a/src/common/fonts/NotoSans-Italic.ttf b/src/common/fonts/NotoSans-Italic.ttf new file mode 100644 index 00000000..d9b9e148 Binary files /dev/null and b/src/common/fonts/NotoSans-Italic.ttf differ diff --git a/src/common/fonts/NotoSans-Medium.ttf b/src/common/fonts/NotoSans-Medium.ttf new file mode 100644 index 00000000..a44124bb Binary files /dev/null and b/src/common/fonts/NotoSans-Medium.ttf differ diff --git a/src/common/fonts/NotoSans-MediumItalic.ttf b/src/common/fonts/NotoSans-MediumItalic.ttf new file mode 100644 index 00000000..467af1b3 Binary files /dev/null and b/src/common/fonts/NotoSans-MediumItalic.ttf differ diff --git a/src/common/fonts/NotoSans-Regular.ttf b/src/common/fonts/NotoSans-Regular.ttf new file mode 100644 index 00000000..4bac02f2 Binary files /dev/null and b/src/common/fonts/NotoSans-Regular.ttf differ diff --git a/src/common/fonts/NotoSans-VariableFont_wdth,wght.ttf b/src/common/fonts/NotoSans-VariableFont_wdth,wght.ttf new file mode 100644 index 00000000..9530d84d Binary files /dev/null and b/src/common/fonts/NotoSans-VariableFont_wdth,wght.ttf differ diff --git a/src/common/image-example-1.png b/src/common/image-example-1.png new file mode 100644 index 00000000..353289e3 Binary files /dev/null and b/src/common/image-example-1.png differ diff --git a/src/common/image-example-2.png b/src/common/image-example-2.png new file mode 100644 index 00000000..9c0dbc82 Binary files /dev/null and b/src/common/image-example-2.png differ diff --git a/src/common/image-example-3.png b/src/common/image-example-3.png new file mode 100644 index 00000000..5720f567 Binary files /dev/null and b/src/common/image-example-3.png differ diff --git a/src/common/logo.svg b/src/common/logo.svg new file mode 100644 index 00000000..eae2fb04 --- /dev/null +++ b/src/common/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/common/mount.tsx b/src/common/mount.tsx new file mode 100644 index 00000000..e4426c14 --- /dev/null +++ b/src/common/mount.tsx @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +/* eslint-disable @eslint-react/naming-convention/filename-extension */ +import React from 'react'; +import { createRoot, Root } from 'react-dom/client'; + +const rootMap = new WeakMap(); + +export function mount(element: React.ReactElement, container: HTMLElement): void { + let root = rootMap.get(container); + + if (!root) { + root = createRoot(container); + rootMap.set(container, root); + } + + root.render(element); +} + +export function unmount(container: HTMLElement): void { + const root = rootMap.get(container); + + if (root) { + root.unmount(); + rootMap.delete(container); + } +} diff --git a/src/common/theme-core.ts b/src/common/theme-core.ts new file mode 100644 index 00000000..0819bc8c --- /dev/null +++ b/src/common/theme-core.ts @@ -0,0 +1,540 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +// ============================================================================ +// Theme for new Core default +// ============================================================================ + +export function generateThemeConfig(customAccentColor?: { light: string; dark: string }, fontFamily?: string) { + const isEmberModern = fontFamily?.includes('Ember Modern Text') ?? false; + const isNotoSans = fontFamily?.includes('Noto Sans') ?? false; + const headingFontWeight = isEmberModern ? '700' : isNotoSans ? '600' : '500'; + // Primary accent colors + + const colorSelectedAccent = customAccentColor || { light: '#1b232d', dark: '#F9F9FB' }; + + const colorSelectedAccentSubtle = { light: '#F6F6F9', dark: '#06080A' }; + + const colorSelectedAccentSubtleHover = { light: '#EBEBF0', dark: '#131920' }; + + // Secondary accent colors (darker/more saturated variant) + const colorSelectedAccentSecondary = { light: '#1b232d', dark: '#F9F9FB' }; + + // Neutral colors + const colorNeutralDefault = { light: '#1b232d', dark: '#f3f3f7' }; + const colorNeutralInverse = { light: '#ffffff', dark: '#131920' }; + const colorNeutralBackground = { light: '#F6F6F9', dark: '#333843' }; + + // Toned down text color + const colorTextBodySecondary = { light: '#656871', dark: '#B4B4BB' }; + + // Status colors + const colorSuccess = { light: '#008559', dark: '#008559' }; + + return { + tokens: { + fontFamilyBase: "'Noto Sans', 'Helvetica Neue', Roboto, Arial, sans-serif", + colorTextBodyDefault: { light: '#161D26', dark: '#c6c6cd' }, + colorTextBodySecondary: colorTextBodySecondary, + + // ======================================================================== + // BUTTONS - Normal + // ======================================================================== + colorBorderButtonNormalDefault: colorNeutralDefault, + colorBorderButtonNormalHover: colorSelectedAccent, + colorBorderButtonNormalActive: colorSelectedAccentSecondary, + + colorBackgroundButtonNormalHover: colorNeutralBackground, + colorBackgroundButtonNormalActive: colorSelectedAccentSubtleHover, + + colorTextButtonNormalDefault: colorNeutralDefault, + colorTextButtonNormalHover: colorSelectedAccent, + colorTextButtonNormalActive: colorSelectedAccentSecondary, + + // ======================================================================== + // BUTTONS - Primary + // ======================================================================== + colorBackgroundButtonPrimaryDefault: colorSelectedAccentSecondary, + colorBackgroundButtonPrimaryHover: { light: '#06080A', dark: '#FFFFFF' }, + colorBackgroundButtonPrimaryActive: colorSelectedAccentSecondary, + + colorTextButtonPrimaryDefault: colorNeutralInverse, + colorTextButtonPrimaryHover: colorNeutralInverse, + colorTextButtonPrimaryActive: colorNeutralInverse, + + // ======================================================================== + // BUTTONS - Link + // ======================================================================== + colorBackgroundButtonLinkHover: { light: '#F6F6F9', dark: '#333843' }, + colorBackgroundButtonLinkActive: colorSelectedAccentSubtleHover, + + colorTextLinkButtonNormalDefault: colorSelectedAccent, + + // ======================================================================== + // BUTTONS - Toggle + // ======================================================================== + colorBackgroundToggleButtonNormalPressed: colorSelectedAccentSubtleHover, + colorBorderToggleButtonNormalPressed: colorSelectedAccent, + colorTextToggleButtonNormalPressed: colorSelectedAccent, + + // ======================================================================== + // CONTROLS - Checkboxes, Radio, Toggle + // ======================================================================== + colorBackgroundControlChecked: colorSelectedAccent, + //colorBackgroundToggleCheckedDisabled: colorSelectedAccentDisabled, + + // ======================================================================== + // LINKS & TEXT + // ======================================================================== + colorTextLinkDefault: { light: '#06080A', dark: '#EBEBF0' }, + colorTextLinkHover: { light: '#424650', dark: '#FFFFFF' }, + colorTextAccent: colorSelectedAccent, + + // ======================================================================== + // SELECTION & FOCUS + // ======================================================================== + colorBorderItemFocused: colorSelectedAccent, + colorBorderItemSelected: colorSelectedAccent, + colorBackgroundItemSelected: colorSelectedAccentSubtle, + colorBackgroundLayoutToggleSelectedDefault: colorSelectedAccent, + + // ======================================================================== + // SEGMENTS & TABS + // ======================================================================== + colorBackgroundSegmentActive: colorSelectedAccent, + + // ======================================================================== + // SLIDER + // ======================================================================== + colorBackgroundSliderRangeDefault: colorSelectedAccent, + colorBackgroundSliderHandleDefault: colorSelectedAccent, + + // ======================================================================== + // PROGRESS BAR + // ======================================================================== + colorBackgroundProgressBarValueDefault: colorSelectedAccent, + + // ======================================================================== + // NOTIFICATIONS + // ======================================================================== + colorBackgroundNotificationGreen: colorSuccess, + colorBackgroundNotificationBlue: { light: '#0033CC', dark: '#0033CC' }, + colorTextNotificationDefault: { light: '#ffffff', dark: '#ffffff' }, + + // ======================================================================== + // STATUS + // ======================================================================== + colorTextStatusInfo: { light: '#0033CC', dark: '#7598FF' }, + // colorTextStatusWarning: { light: '#855900', dark: '#ffe347' }, + // colorTextStatusError: { light: '#DB0000', dark: '#ff7a7a' }, + + // ======================================================================== + // TYPOGRAPHY - Headings + // ======================================================================== + colorTextBreadcrumbCurrent: colorSelectedAccent, + + // H1 + fontSizeHeadingXl: '26px', + lineHeightHeadingXl: '32px', + fontWeightHeadingXl: headingFontWeight, + + // H2 + fontSizeHeadingL: '22px', + lineHeightHeadingL: '26px', + fontWeightHeadingL: headingFontWeight, + //letterSpacingHeadingL: '20px', + + // H3 + fontSizeHeadingM: '18px', + lineHeightHeadingM: '24px', + fontWeightHeadingM: headingFontWeight, + + // H4 + fontSizeHeadingS: '16px', + lineHeightHeadingS: '20px', + fontWeightHeadingS: headingFontWeight, + + // H5 + fontSizeHeadingXs: '14px', + lineHeightHeadingXs: '20px', + fontWeightHeadingXs: headingFontWeight, + + //fontFamilyHeading: 'Ember Modern Display', + + // ======================================================================== + // TYPOGRAPHY - Other + // ======================================================================== + fontWeightButton: headingFontWeight, + + // ======================================================================== + // BORDERS - Width + // ======================================================================== + borderWidthButton: '1px', + borderWidthToken: '1px', + borderWidthAlert: '0px', + borderItemWidth: '1px', + + // ======================================================================== + // BORDERS - Radius + // ======================================================================== + borderRadiusAlert: '2px', + borderRadiusBadge: '4px', + borderRadiusButton: '8px', + borderRadiusContainer: '12px', + borderRadiusDropdown: '8px', + borderRadiusDropzone: '8px', + borderRadiusFlashbar: '4px', + borderRadiusItem: '8px', + borderRadiusInput: '8px', + borderRadiusPopover: '8px', + borderRadiusTabsFocusRing: '10px', + borderRadiusToken: '8px', + borderRadiusTutorialPanelItem: '4px', + + // ======================================================================== + // ICONS - Stroke Width + // ======================================================================== + borderWidthIconSmall: '1.5px', + borderWidthIconNormal: '1.5px', + borderWidthIconMedium: '2px', + borderWidthIconBig: '2px', + borderWidthIconLarge: '2.5px', + }, + + referenceTokens: { + color: { + primary: { + seed: '#1b232d', + }, + }, + }, + + contexts: { + 'top-navigation': { + tokens: { + colorBackgroundContainerContent: { light: '#ffffff', dark: '#161d26' }, + colorBorderDividerDefault: { light: '#c6c6cd', dark: '#424650' }, + colorTextTopNavigationTitle: colorNeutralDefault, + + // Interactive elements + colorTextInteractiveDefault: colorNeutralDefault, + colorTextInteractiveHover: colorSelectedAccent, + colorTextInteractiveActive: { light: '#1b232d', dark: '#7598ff' }, + colorTextAccent: colorNeutralDefault, + }, + }, + header: { + tokens: { + // Normal button + colorBorderButtonNormalDefault: '#f3f3f7', + // colorBorderButtonNormalHover: '#7598ff', + // colorBorderButtonNormalActive: '#7598ff', + + colorBackgroundButtonNormalHover: '#1b232d', + colorBackgroundButtonNormalActive: '#000833', + + colorTextButtonNormalDefault: '#f3f3f7', + // colorTextButtonNormalHover: '#7598ff', + // colorTextButtonNormalActive: '#7598ff', + + // Primary button + colorBackgroundButtonPrimaryDefault: '#f9f9fb', + // colorBackgroundButtonPrimaryHover: '#c2d1ff', + // colorBackgroundButtonPrimaryActive: '#f9f9fb', + + colorTextButtonPrimaryDefault: '#131920', + colorTextButtonPrimaryHover: '#131920', + colorTextButtonPrimaryActive: '#131920', + }, + }, + flashbar: { + tokens: { + // Custom flashbar colors + colorBackgroundNotificationGreen: colorSuccess, + colorBackgroundNotificationBlue: { light: '#0033cc', dark: '#0033cc' }, + colorTextNotificationDefault: { light: '#ffffff', dark: '#ffffff' }, + }, + }, + alert: { + tokens: { + colorBackgroundStatusInfo: { light: '#f6f6f9', dark: '#232b37' }, + colorBackgroundStatusWarning: { light: '#f6f6f9', dark: '#232b37' }, + colorBackgroundStatusError: { light: '#f6f6f9', dark: '#232b37' }, + colorBackgroundStatusSuccess: { light: '#f6f6f9', dark: '#232b37' }, + colorTextStatusInfo: { light: '#0033CC', dark: '#7598FF' }, + colorBorderStatusInfo: { light: '#0033CC', dark: '#7598FF' }, + }, + }, + }, + }; +} + +// ============================================================================ +// Theme for Console +// ============================================================================ + +export function generateThemeConfigConsole() { + return { + tokens: { + fontFamilyBase: "var(--font-amazon-ember, 'Amazon Ember', sans-serif)", + + // ======================================================================== + // BUTTONS - Normal + // ======================================================================== + colorBorderButtonNormalDefault: { light: '#006CE0', dark: '#42B4FF' }, + colorBorderButtonNormalHover: { light: '#002A66', dark: '#75CFFF' }, + colorBorderButtonNormalActive: { light: '#002A66', dark: '#75CFFF' }, + + colorBackgroundButtonNormalHover: { light: '#F0FBFF', dark: '#1B232D' }, + colorBackgroundButtonNormalActive: { light: '#D1F1FF', dark: '#333843' }, + + colorTextButtonNormalDefault: { light: '#006CE0', dark: '#42B4FF' }, + colorTextButtonNormalHover: { light: '#002A66', dark: '#75CFFF' }, + colorTextButtonNormalActive: { light: '#002A66', dark: '#75CFFF' }, + + // ======================================================================== + // BUTTONS - Primary + // ======================================================================== + colorBackgroundButtonPrimaryDefault: { light: '#FF9900', dark: '#FFB347' }, + colorBackgroundButtonPrimaryHover: { light: '#FA6F00', dark: '#FFC870' }, + colorBackgroundButtonPrimaryActive: { light: '#FA6F00', dark: '#FFC870' }, + + colorTextButtonPrimaryDefault: { light: '#0F141A', dark: '#0F141A' }, + colorTextButtonPrimaryHover: { light: '#0F141A', dark: '#0F141A' }, + colorTextButtonPrimaryActive: { light: '#0F141A', dark: '#0F141A' }, + + // ======================================================================== + // BUTTONS - Link + // ======================================================================== + colorBackgroundButtonLinkHover: { light: '#F0FBFF', dark: '#1B232D' }, + colorBackgroundButtonLinkActive: { light: '#D1F1FF', dark: '#333843' }, + + colorTextLinkButtonNormalDefault: { light: '#006CE0', dark: '#42B4FF' }, + + // ======================================================================== + // BUTTONS - Toggle + // ======================================================================== + colorBackgroundToggleButtonNormalPressed: { light: '#D1F1FF', dark: '#333843' }, + colorBorderToggleButtonNormalPressed: { light: '#006CE0', dark: '#42B4FF' }, + colorTextToggleButtonNormalPressed: { light: '#002A66', dark: '#75CFFF' }, + + // ======================================================================== + // CONTROLS - Checkboxes, Radio, Toggle + // ======================================================================== + colorBackgroundControlChecked: { light: '#006CE0', dark: '#42B4FF' }, + //colorBackgroundToggleCheckedDisabled: { light: '#B8E7FF', dark: '#002A66' }, + + // ======================================================================== + // LINKS & TEXT + // ======================================================================== + colorTextLinkDefault: { light: '#006CE0', dark: '#42B4FF' }, + colorTextLinkHover: { light: '#002A66', dark: '#75CFFF' }, + colorTextAccent: { light: '#006CE0', dark: '#42B4FF' }, + + // ======================================================================== + // SELECTION & FOCUS + // ======================================================================== + colorBorderItemFocused: { light: '#006CE0', dark: '#42B4FF' }, + colorBorderItemSelected: { light: '#006CE0', dark: '#42B4FF' }, + colorBackgroundItemSelected: { light: '#F0FBFF', dark: '#001129' }, + colorBackgroundLayoutToggleSelectedDefault: { light: '#006CE0', dark: '#42B4FF' }, + + // ======================================================================== + // SEGMENTS & TABS + // ======================================================================== + colorBackgroundSegmentActive: { light: '#006CE0', dark: '#42B4FF' }, + + // ======================================================================== + // SLIDER + // ======================================================================== + colorBackgroundSliderRangeDefault: { light: '#006CE0', dark: '#42B4FF' }, + colorBackgroundSliderHandleDefault: { light: '#006CE0', dark: '#42B4FF' }, + //colorBackgroundSliderRangeActive: { light: '#004A9E', dark: '#75CFFF' }, + //colorBackgroundSliderHandleActive: { light: '#004A9E', dark: '#75CFFF' }, + + // ======================================================================== + // PROGRESS BAR + // ======================================================================== + colorBackgroundProgressBarValueDefault: { light: '#006CE0', dark: '#42B4FF' }, + + // ======================================================================== + // NOTIFICATIONS + // ======================================================================== + colorBackgroundNotificationGreen: { light: '#00802F', dark: '#2BB534' }, + colorBackgroundNotificationBlue: { light: '#006CE0', dark: '#42B4FF' }, + + // ======================================================================== + // STATUS + // ======================================================================== + colorTextStatusInfo: { light: '#006CE0', dark: '#42B4FF' }, + // colorTextStatusWarning: { light: '#855900', dark: '#FFE347' }, + // colorTextStatusError: { light: '#DB0000', dark: '#FF7A7A' }, + + colorTextBreadcrumbCurrent: { light: '#656871', dark: '#8c8c94' }, + + // ======================================================================== + // TYPOGRAPHY - Headings + // ======================================================================== + // Display Large + // fontSizeDisplayL: '42px', + // lineHeightDisplayL: '48px', + fontWeightDisplayL: '700', + + // H1 + fontSizeHeadingXl: '24px', + lineHeightHeadingXl: '30px', + fontWeightHeadingXl: '700', + + // H2 + fontSizeHeadingL: '20px', + lineHeightHeadingL: '24px', + fontWeightHeadingL: '700', + //letterSpacingHeadingL: '-0.015em', + + // H3 + fontSizeHeadingM: '18px', + lineHeightHeadingM: '22px', + fontWeightHeadingM: '700', + + // H4 + fontSizeHeadingS: '16px', + lineHeightHeadingS: '20px', + fontWeightHeadingS: '700', + + // H5 + fontSizeHeadingXs: '14px', + lineHeightHeadingXs: '18px', + fontWeightHeadingXs: '700', + + //fontFamilyHeading: 'Ember Modern Display', + + // ======================================================================== + // TYPOGRAPHY - Other + // ======================================================================== + fontWeightButton: '700', + + // ======================================================================== + // BORDERS - Width + // ======================================================================== + borderWidthButton: '2px', + borderWidthToken: '2px', + borderWidthAlert: '2px', + borderItemWidth: '2px', + + // ======================================================================== + // BORDERS - Radius + // ======================================================================== + borderRadiusAlert: '12px', + borderRadiusBadge: '4px', + borderRadiusButton: '20px', + borderRadiusContainer: '16px', + borderRadiusDropdown: '8px', + borderRadiusDropzone: '12px', + borderRadiusFlashbar: '12px', + borderRadiusItem: '8px', + borderRadiusInput: '8px', + borderRadiusPopover: '8px', + borderRadiusTabsFocusRing: '20px', + borderRadiusToken: '8px', + borderRadiusTutorialPanelItem: '8px', + + // ======================================================================== + // ICONS - Stroke Width + // ======================================================================== + borderWidthIconSmall: '2px', + borderWidthIconNormal: '2px', + borderWidthIconMedium: '2px', + borderWidthIconBig: '3px', + borderWidthIconLarge: '4px', + }, + + contexts: { + 'top-navigation': { + tokens: { + //colorBackgroundContainerContent: { light: '#161D26', dark: '#161D26' }, + }, + }, + header: { + tokens: { + // Normal button + colorBorderButtonNormalDefault: '#f3f3f7', + // colorBorderButtonNormalHover: '#7598ff', + // colorBorderButtonNormalActive: '#7598ff', + + colorBackgroundButtonNormalHover: '#1b232d', + colorBackgroundButtonNormalActive: '#000833', + + colorTextButtonNormalDefault: '#f3f3f7', + // colorTextButtonNormalHover: '#7598ff', + // colorTextButtonNormalActive: '#7598ff', + + // Primary button + colorBackgroundButtonPrimaryDefault: '#f9f9fb', + // colorBackgroundButtonPrimaryHover: '#c2d1ff', + // colorBackgroundButtonPrimaryActive: '#f9f9fb', + + colorTextButtonPrimaryDefault: '#131920', + colorTextButtonPrimaryHover: '#131920', + colorTextButtonPrimaryActive: '#131920', + }, + }, + flashbar: { + tokens: { + // Custom flashbar colors + //colorBackgroundNotificationBlue: { light: '#0033cc', dark: '#0033cc' }, + }, + }, + alert: { + tokens: { + colorBackgroundStatusInfo: { light: '#F0FBFF', dark: '#001129' }, + colorBackgroundStatusWarning: { light: '#FFFEF0', dark: '#191100' }, + colorBackgroundStatusError: { light: '#FFF5F5', dark: '#1F0000' }, + colorBackgroundStatusSuccess: { light: '#EFFFF1', dark: '#001401' }, + colorTextStatusInfo: { light: '#006CE0', dark: '#42B4FF' }, + colorBorderStatusInfo: { light: '#006CE0', dark: '#42B4FF' }, + }, + }, + }, + }; +} + +export const themeCoreConfig = generateThemeConfig(); + +export const colorTextLinkSecondOption = { light: '#295EFF', dark: '#7598FF' }; + +// ============================================================================ +// Theme Comparison Utilities +// ============================================================================ + +/** + * Utility for comparing different theme design directions. + * Ensures complete theme isolation by resetting before each application. + */ +export function createThemeComparison() { + return { + /** + * Apply Design Direction A with optional custom accent color + */ + applyDirectionA: (customAccentColor?: { light: string; dark: string }) => { + const themeA = generateThemeConfig(customAccentColor); + // applyCustomTheme handles reset automatically + return themeA; + }, + + /** + * Apply Design Direction B + */ + applyDirectionB: () => { + const themeB = generateThemeConfigConsole(); + // applyCustomTheme handles reset automatically + return themeB; + }, + + /** + * Get theme config without applying (for inspection/comparison) + */ + getThemeConfig: (direction: 'A' | 'B', customAccentColor?: { light: string; dark: string }) => { + return direction === 'A' ? generateThemeConfig(customAccentColor) : generateThemeConfigConsole(); + }, + }; +} diff --git a/src/pages/cards/index.tsx b/src/pages/cards/index.tsx index f7e11d3e..33cbea92 100644 --- a/src/pages/cards/index.tsx +++ b/src/pages/cards/index.tsx @@ -3,6 +3,13 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; +import { applyTheme } from '@cloudscape-design/components/theming'; + +import { themeCoreConfig } from '../../common/theme-core'; import { App } from './root'; +import '../../styles/base.scss'; + +applyTheme({ theme: themeCoreConfig }); + createRoot(document.getElementById('app')!).render(); diff --git a/src/pages/cards/root.tsx b/src/pages/cards/root.tsx index fecb5226..c423fb4d 100644 --- a/src/pages/cards/root.tsx +++ b/src/pages/cards/root.tsx @@ -6,7 +6,9 @@ import { useCollection } from '@cloudscape-design/collection-hooks'; import { AppLayoutProps } from '@cloudscape-design/components/app-layout'; import Cards from '@cloudscape-design/components/cards'; import CollectionPreferences from '@cloudscape-design/components/collection-preferences'; +import Flashbar, { FlashbarProps } from '@cloudscape-design/components/flashbar'; import Pagination from '@cloudscape-design/components/pagination'; +import SplitPanel from '@cloudscape-design/components/split-panel'; import TextFilter from '@cloudscape-design/components/text-filter'; import { Distribution } from '../../fake-server/types'; @@ -19,10 +21,12 @@ import { import { FullPageHeader } from '../commons'; import { CustomAppLayout, + DemoTopNavigation, + GlobalSplitPanelContent, Navigation, - Notifications, TableEmptyState, TableNoMatchState, + useGlobalSplitPanel, } from '../commons/common-components'; import DataProvider from '../commons/data-provider'; import { useLocalStorage } from '../commons/use-local-storage'; @@ -30,6 +34,54 @@ import { CARD_DEFINITIONS, DEFAULT_PREFERENCES, PAGE_SIZE_OPTIONS, VISIBLE_CONTE import { Breadcrumbs, ToolsContent } from './common-components'; import '../../styles/base.scss'; +import '../../styles/top-navigation.scss'; + +function StackedNotifications() { + const [items, setItems] = useState([ + { + type: 'success', + dismissible: true, + dismissLabel: 'Dismiss message', + content: 'This is a success flash message', + id: 'message_5', + onDismiss: () => setItems(items => items.filter(item => item.id !== 'message_5')), + }, + { + type: 'warning', + dismissible: true, + dismissLabel: 'Dismiss message', + content: 'This is a warning flash message', + id: 'message_4', + onDismiss: () => setItems(items => items.filter(item => item.id !== 'message_4')), + }, + { + type: 'error', + dismissible: true, + dismissLabel: 'Dismiss message', + header: 'Failed to update instance id-4890f893e', + content: 'This is a dismissible error message', + id: 'message_3', + onDismiss: () => setItems(items => items.filter(item => item.id !== 'message_3')), + }, + ]); + + return ( + + ); +} interface DetailsCardsProps { loadHelpPanelContent: () => void; @@ -113,26 +165,41 @@ function DetailsCards({ loadHelpPanelContent }: DetailsCardsProps) { export function App() { const [toolsOpen, setToolsOpen] = useState(false); + const { splitPanelOpen, onSplitPanelToggle, splitPanelSize, onSplitPanelResize, splitPanelPreferences } = + useGlobalSplitPanel(); const appLayout = useRef(null); return ( - } - notifications={} - breadcrumbs={} - content={ - { - setToolsOpen(true); - appLayout.current?.focusToolsClose(); - }} - /> - } - contentType="cards" - tools={} - toolsOpen={toolsOpen} - onToolsChange={({ detail }) => setToolsOpen(detail.open)} - stickyNotifications={true} - /> + <> + + } + notifications={} + breadcrumbs={} + content={ + { + setToolsOpen(true); + appLayout.current?.focusToolsClose(); + }} + /> + } + contentType="cards" + tools={} + toolsOpen={toolsOpen} + onToolsChange={({ detail }) => setToolsOpen(detail.open)} + splitPanelOpen={splitPanelOpen} + onSplitPanelToggle={onSplitPanelToggle} + splitPanelSize={splitPanelSize} + onSplitPanelResize={onSplitPanelResize} + splitPanelPreferences={splitPanelPreferences} + splitPanel={ + + + + } + stickyNotifications={true} + /> + ); } diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index 00340308..b00d6ac2 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -5,24 +5,30 @@ import { createRoot } from 'react-dom/client'; import { I18nProvider } from '@cloudscape-design/components/i18n'; import enMessages from '@cloudscape-design/components/i18n/messages/all.en.json'; +import { applyTheme } from '@cloudscape-design/components/theming'; -import { CustomAppLayout, Notifications } from '../commons/common-components'; +import { themeCoreConfig } from '../../common/theme-core'; +import { CustomAppLayout, DemoTopNavigation, Notifications } from '../commons/common-components'; import Chat from './chat'; import '../../styles/base.scss'; - +import '../../styles/top-navigation.scss'; function App() { return ( - } - notifications={} - /> + <> + + } + notifications={} + /> + ); } +applyTheme({ theme: themeCoreConfig }); createRoot(document.getElementById('app')!).render(); diff --git a/src/pages/commons/common-components.tsx b/src/pages/commons/common-components.tsx index 479f3613..61cf4c9b 100644 --- a/src/pages/commons/common-components.tsx +++ b/src/pages/commons/common-components.tsx @@ -15,6 +15,9 @@ import { isVisualRefresh } from '../../common/apply-mode'; // backward compatibility export * from './index'; +export { DemoTopNavigation } from './top-navigation'; +export { GlobalSplitPanelContent } from './split-panel-content'; +export { useGlobalSplitPanel } from './use-global-split-panel'; export const ec2NavItems = [ { type: 'link', text: 'Instances', href: '#/instances' }, diff --git a/src/pages/commons/external-link-group.tsx b/src/pages/commons/external-link-group.tsx index c5f6c80e..ab86a143 100644 --- a/src/pages/commons/external-link-group.tsx +++ b/src/pages/commons/external-link-group.tsx @@ -22,7 +22,7 @@ interface ExternalLinkGroupProps { function ExternalLinkItem({ href, text }: ExternalLinkItemProps) { return ( - + {text} ); diff --git a/src/pages/commons/global-drawer-plugin.tsx b/src/pages/commons/global-drawer-plugin.tsx new file mode 100644 index 00000000..1f5505a1 --- /dev/null +++ b/src/pages/commons/global-drawer-plugin.tsx @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React from 'react'; + +import Box from '@cloudscape-design/components/box'; +import Drawer from '@cloudscape-design/components/drawer'; +import Header from '@cloudscape-design/components/header'; +import awsuiPlugins from '@cloudscape-design/components/internal/plugins'; +import SpaceBetween from '@cloudscape-design/components/space-between'; + +import { mount, unmount } from '../../common/mount'; + +function GlobalDrawerContent() { + return ( + Design exploration}> + + This is a custom global drawer component that appears across all demo pages. + You can add any content here that you want to be accessible from all pages. + + This drawer is registered using the Cloudscape AppLayout plugin system and appears in the toolbar. + + + + ); +} + +// Register the global drawer plugin +export function registerGlobalDrawer() { + awsuiPlugins.appLayout.registerDrawer({ + id: 'global-drawer-demo', + //type: 'global', + defaultActive: false, + resizable: true, + defaultSize: 400, + preserveInactiveContent: true, + ariaLabels: { + closeButton: 'Close global drawer', + content: 'Global drawer content', + triggerButton: 'Open global drawer', + resizeHandle: 'Resize global drawer', + }, + trigger: { + iconSvg: ``, + }, + mountContent: container => { + mount(, container); + }, + unmountContent: container => { + unmount(container); + }, + }); +} diff --git a/src/pages/commons/index.ts b/src/pages/commons/index.ts index 965e1017..a62576c4 100644 --- a/src/pages/commons/index.ts +++ b/src/pages/commons/index.ts @@ -8,3 +8,4 @@ export { HelpPanelProvider, useHelpPanel } from './help-panel'; export { InfoLink } from './info-link'; export { Navigation, navItems } from './navigation'; export { Notifications } from './notifications'; +export { DemoTopNavigation } from './top-navigation'; diff --git a/src/pages/commons/navigation.tsx b/src/pages/commons/navigation.tsx index c85ae947..2b81cd28 100644 --- a/src/pages/commons/navigation.tsx +++ b/src/pages/commons/navigation.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -import React from 'react'; +import React, { useState } from 'react'; import SideNavigation, { SideNavigationProps } from '@cloudscape-design/components/side-navigation'; @@ -29,11 +29,6 @@ export const navItems: SideNavigationProps['items'] = [ }, ]; -const defaultOnFollowHandler: SideNavigationProps['onFollow'] = event => { - // keep the locked href for our demo pages - event.preventDefault(); -}; - interface NavigationProps { activeHref?: string; header?: SideNavigationProps['header']; @@ -42,10 +37,18 @@ interface NavigationProps { } export function Navigation({ - activeHref, + activeHref: activeHrefProp, header = navHeader, items = navItems, - onFollowHandler = defaultOnFollowHandler, + onFollowHandler, }: NavigationProps) { - return ; + const [activeHref, setActiveHref] = useState(activeHrefProp || '#/distributions'); + + const handleFollow: SideNavigationProps['onFollow'] = event => { + event.preventDefault(); + setActiveHref(event.detail.href); + onFollowHandler?.(event); + }; + + return ; } diff --git a/src/pages/commons/split-panel-content.tsx b/src/pages/commons/split-panel-content.tsx new file mode 100644 index 00000000..15fbe923 --- /dev/null +++ b/src/pages/commons/split-panel-content.tsx @@ -0,0 +1,677 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React, { useEffect, useState } from 'react'; + +import Box from '@cloudscape-design/components/box'; +import Button from '@cloudscape-design/components/button'; +import Checkbox from '@cloudscape-design/components/checkbox'; +import ColumnLayout from '@cloudscape-design/components/column-layout'; +import FormField from '@cloudscape-design/components/form-field'; +import Input from '@cloudscape-design/components/input'; +import Select, { SelectProps } from '@cloudscape-design/components/select'; +import Slider from '@cloudscape-design/components/slider'; +import SpaceBetween from '@cloudscape-design/components/space-between'; + +import { applyCustomTheme } from '../../common/apply-theme'; +import { + colorTextLinkSecondOption, + generateThemeConfig, + generateThemeConfigConsole, + themeCoreConfig, +} from '../../common/theme-core'; + +interface ThemeConfig { + colorSelectedAccent?: string; + borderWidthButton?: string; + borderWidthField?: string; + borderWidthIconSmall?: string; + borderWidthIconNormal?: string; + borderWidthIconMedium?: string; + borderWidthIconBig?: string; + borderWidthIconLarge?: string; + borderRadiusButton?: string; + borderRadiusInput?: string; + borderRadiusContainer?: string; + spaceScaledXxxs?: string; + spaceScaledXxs?: string; + spaceScaledXs?: string; + spaceScaledS?: string; + spaceScaledM?: string; + spaceScaledL?: string; + spaceScaledXl?: string; + spaceScaledXxl?: string; + colorBackgroundButtonPrimaryDefault?: string; + colorBackgroundButtonPrimaryHover?: string; + colorTextButtonPrimaryDefault?: string; + fontSizeBodyS?: string; + fontSizeBodyM?: string; + fontSizeHeadingXl?: string; + fontSizeHeadingL?: string; + fontSizeHeadingM?: string; + fontSizeHeadingS?: string; + fontSizeHeadingXs?: string; + lineHeightHeadingXl?: string; + lineHeightHeadingL?: string; + lineHeightHeadingM?: string; + lineHeightHeadingS?: string; + lineHeightHeadingXs?: string; + shadowContainer?: string; + fontFamilyBase?: string; + colorTextLinkDefault?: string; + colorTextLinkHover?: string; +} + +export function GlobalSplitPanelContent() { + // Helper function to extract numeric value from CSS value like "24px" -> "24" + const extractNumericValue = (value: string | undefined): string => { + if (!value) { + return ''; + } + const match = value.match(/^(\d+(?:\.\d+)?)/); + return match ? match[1] : value; + }; + + // Helper function to format color object to string + const formatColorValue = (value: { light: string; dark: string } | undefined): string => { + if (!value) { + return ''; + } + return `light: '${value.light}', dark: '${value.dark}'`; + }; + + const [fontStretch, setFontStretch] = useState(96); + const [consoleTheme, setConsoleTheme] = useState(false); + const [checked, setChecked] = useState(false); + const [checkedFontSmooth, setCheckedFontSmooth] = useState(true); + const [customLinkColor, setCustomLinkColor] = useState(false); + const [config, setConfig] = useState({ + colorSelectedAccent: formatColorValue({ light: '#1b232d', dark: '#f3f3f7' }), + borderWidthButton: extractNumericValue((themeCoreConfig.tokens?.borderWidthButton as string) || '2px'), + borderWidthIconSmall: extractNumericValue((themeCoreConfig.tokens?.borderWidthIconSmall as string) || '1.5px'), + borderWidthIconNormal: extractNumericValue((themeCoreConfig.tokens?.borderWidthIconNormal as string) || '1.5px'), + borderWidthIconMedium: extractNumericValue((themeCoreConfig.tokens?.borderWidthIconMedium as string) || '2px'), + borderWidthIconBig: extractNumericValue((themeCoreConfig.tokens?.borderWidthIconBig as string) || '2px'), + borderWidthIconLarge: extractNumericValue((themeCoreConfig.tokens?.borderWidthIconLarge as string) || '2.5px'), + borderRadiusButton: extractNumericValue((themeCoreConfig.tokens?.borderRadiusButton as string) || '8px'), + borderRadiusContainer: extractNumericValue((themeCoreConfig.tokens?.borderRadiusContainer as string) || '12px'), + fontSizeHeadingXl: extractNumericValue((themeCoreConfig.tokens?.fontSizeHeadingXl as string) || '26px'), + fontSizeHeadingL: extractNumericValue((themeCoreConfig.tokens?.fontSizeHeadingL as string) || '22px'), + fontSizeHeadingM: extractNumericValue((themeCoreConfig.tokens?.fontSizeHeadingM as string) || '20px'), + fontSizeHeadingS: extractNumericValue((themeCoreConfig.tokens?.fontSizeHeadingS as string) || '18px'), + fontSizeHeadingXs: extractNumericValue((themeCoreConfig.tokens?.fontSizeHeadingXs as string) || '16px'), + lineHeightHeadingXl: extractNumericValue((themeCoreConfig.tokens?.lineHeightHeadingXl as string) || '32px'), + lineHeightHeadingL: extractNumericValue((themeCoreConfig.tokens?.lineHeightHeadingL as string) || '26px'), + lineHeightHeadingM: extractNumericValue((themeCoreConfig.tokens?.lineHeightHeadingM as string) || '24px'), + lineHeightHeadingS: extractNumericValue((themeCoreConfig.tokens?.lineHeightHeadingS as string) || '22px'), + lineHeightHeadingXs: extractNumericValue((themeCoreConfig.tokens?.lineHeightHeadingXs as string) || '20px'), + fontFamilyBase: (themeCoreConfig.tokens?.fontFamilyBase as string) || '', + }); + + const [errors, setErrors] = useState>({}); + + const fontFamilyOptions: SelectProps.Options = [ + { label: 'NotoSans', value: "'NotoSans', 'Noto Sans', sans-serif" }, + { label: 'AmazonEmberDisplay', value: "'AmazonEmberDisplay', 'Amazon Ember Display', sans-serif" }, + { label: 'EmberModernText', value: "'Ember Modern Text', sans-serif" }, + ]; + + // Apply CSS class to body when toggle changes + useEffect(() => { + const applyThemeChanges = () => { + try { + // Parse colorSelectedAccent if present + let customAccentColor; + if (config.colorSelectedAccent) { + const lightMatch = config.colorSelectedAccent.match(/light:\s*'([^']+)'/); + const darkMatch = config.colorSelectedAccent.match(/dark:\s*'([^']+)'/); + if (lightMatch && darkMatch) { + customAccentColor = { light: lightMatch[1], dark: darkMatch[1] }; + } + } + + // Select base theme based on checkbox selection + let baseTheme; + let shouldApplyCustomTokens = true; + + if (consoleTheme) { + // Console theme: Minimal theme with only specific tokens + // Don't apply form customizations for Console theme + baseTheme = generateThemeConfigConsole(); + shouldApplyCustomTokens = false; + } else { + // New Core theme: Complete theme with form customizations + baseTheme = customAccentColor + ? generateThemeConfig(customAccentColor, config.fontFamilyBase) + : generateThemeConfig(undefined, config.fontFamilyBase); + shouldApplyCustomTokens = true; + } + + // Build the theme object + let themeTokens: any = { ...baseTheme.tokens }; + + // Only apply custom tokens from form for Option A + if (shouldApplyCustomTokens) { + const colorTokenKeys = ['colorTextLinkDefault', 'colorTextLinkHover']; + const customTokens = Object.fromEntries( + Object.entries(config) + .filter(([key, value]) => key !== 'colorSelectedAccent' && value !== undefined && value !== '') + .map(([key, value]) => { + // Parse color tokens that use "light: '...', dark: '...'" format + if (colorTokenKeys.includes(key)) { + const lightMatch = String(value).match(/light:\s*'([^']+)'/); + const darkMatch = String(value).match(/dark:\s*'([^']+)'/); + if (lightMatch && darkMatch) { + return [key, { light: lightMatch[1], dark: darkMatch[1] }]; + } + return [key, undefined]; // skip invalid color values + } + // Font family should not have 'px' appended + if (key === 'fontFamilyBase') { + return [key, value]; + } + // If value is a number without unit, append 'px' + const stringValue = String(value).trim(); + if (stringValue && /^\d+(\.\d+)?$/.test(stringValue)) { + return [key, `${stringValue}px`]; + } + return [key, value]; + }) + .filter(([, value]) => value !== undefined), + ); + themeTokens = { ...themeTokens, ...customTokens }; + } + + // Apply borderRadiusFlashbar only when toggle is on + if (checked) { + themeTokens.borderRadiusFlashbar = '0px'; + } + + const updatedTheme = { + tokens: themeTokens, + referenceTokens: (baseTheme as any).referenceTokens || {}, + contexts: (baseTheme as any).contexts || {}, + }; + + // Apply theme - reset happens automatically in applyCustomTheme + applyCustomTheme(updatedTheme as any); + } catch (error) { + console.error('Failed to apply theme:', error); + } + }; + + if (checked) { + document.body.classList.add('filled-flashbar'); + } else { + document.body.classList.remove('filled-flashbar'); + } + + // Apply custom CSS class when Console checkbox is disabled (unchecked) + if (!consoleTheme) { + document.body.classList.add('custom-css-enabled'); + } else { + document.body.classList.remove('custom-css-enabled'); + } + + // Apply theme changes when toggle state or console theme changes + applyThemeChanges(); + }, [checked, config, consoleTheme, customLinkColor]); + + // Toggle font-smooth-auto class on body + // When checkbox is OFF (default), font smoothing is subpixel-antialiased (normal browser behavior) + // When checkbox is ON, we add the class to set font-smoothing to auto + useEffect(() => { + if (checkedFontSmooth) { + document.body.classList.remove('font-smooth-auto'); + } else { + document.body.classList.add('font-smooth-auto'); + } + }, [checkedFontSmooth]); + + // Apply font-stretch globally via injected style tag + useEffect(() => { + const styleId = 'font-stretch-override'; + let styleEl = document.getElementById(styleId) as HTMLStyleElement | null; + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.id = styleId; + document.head.appendChild(styleEl); + } + styleEl.textContent = `* { font-stretch: ${fontStretch}% !important; }`; + return () => { + styleEl?.remove(); + }; + }, [fontStretch]); + + const handleInputChange = (key: keyof ThemeConfig, value: string) => { + setConfig(prev => ({ ...prev, [key]: value })); + if (errors[key]) { + setErrors(prev => { + const newErrors = { ...prev }; + delete newErrors[key]; + return newErrors; + }); + } + }; + + const applyThemeChanges = () => { + try { + // Parse colorSelectedAccent if present + let customAccentColor; + if (config.colorSelectedAccent) { + const lightMatch = config.colorSelectedAccent.match(/light:\s*'([^']+)'/); + const darkMatch = config.colorSelectedAccent.match(/dark:\s*'([^']+)'/); + if (lightMatch && darkMatch) { + customAccentColor = { light: lightMatch[1], dark: darkMatch[1] }; + } + } + + // Select base theme based on checkbox selection + let baseTheme; + let shouldApplyCustomTokens = true; + + if (consoleTheme) { + // Console theme: Minimal theme with only specific tokens + // Don't apply form customizations for Console theme + baseTheme = generateThemeConfigConsole(); + shouldApplyCustomTokens = false; + } else { + // New Core theme: Complete theme with form customizations + baseTheme = customAccentColor + ? generateThemeConfig(customAccentColor, config.fontFamilyBase) + : generateThemeConfig(undefined, config.fontFamilyBase); + shouldApplyCustomTokens = true; + } + + // Build the theme object + let themeTokens: any = { ...baseTheme.tokens }; + + // Only apply custom tokens from form for Option A + if (shouldApplyCustomTokens) { + const customTokens = Object.fromEntries( + Object.entries(config) + .filter(([key, value]) => key !== 'colorSelectedAccent' && value !== undefined && value !== '') + .map(([key, value]) => { + // Font family should not have 'px' appended + if (key === 'fontFamilyBase') { + return [key, value]; + } + // If value is a number without unit, append 'px' + const stringValue = String(value).trim(); + if (stringValue && /^\d+(\.\d+)?$/.test(stringValue)) { + return [key, `${stringValue}px`]; + } + return [key, value]; + }), + ); + themeTokens = { ...themeTokens, ...customTokens }; + } + + // Apply borderRadiusFlashbar only when toggle is on + if (checked) { + themeTokens.borderRadiusFlashbar = '0px'; + } + + const updatedTheme = { + tokens: themeTokens, + referenceTokens: (baseTheme as any).referenceTokens || {}, + contexts: (baseTheme as any).contexts || {}, + }; + + // Apply theme - reset happens automatically in applyCustomTheme + applyCustomTheme(updatedTheme as any); + } catch (error) { + console.error('Failed to apply theme:', error); + } + }; + + const resetTheme = () => { + setConsoleTheme(false); + setChecked(false); + setCheckedFontSmooth(true); + setFontStretch(100); + setConfig({ + colorSelectedAccent: formatColorValue({ light: '#1b232d', dark: '#f3f3f7' }), + borderWidthButton: extractNumericValue((themeCoreConfig.tokens?.borderWidthButton as string) || '1px'), + borderWidthIconSmall: extractNumericValue((themeCoreConfig.tokens?.borderWidthIconSmall as string) || '1.5px'), + borderWidthIconNormal: extractNumericValue((themeCoreConfig.tokens?.borderWidthIconNormal as string) || '1.5px'), + borderWidthIconMedium: extractNumericValue((themeCoreConfig.tokens?.borderWidthIconMedium as string) || '2px'), + borderWidthIconBig: extractNumericValue((themeCoreConfig.tokens?.borderWidthIconBig as string) || '2px'), + borderWidthIconLarge: extractNumericValue((themeCoreConfig.tokens?.borderWidthIconLarge as string) || '2.5px'), + borderRadiusButton: extractNumericValue((themeCoreConfig.tokens?.borderRadiusButton as string) || '8px'), + borderRadiusInput: extractNumericValue((themeCoreConfig.tokens?.borderRadiusInput as string) || '8px'), + borderRadiusContainer: extractNumericValue((themeCoreConfig.tokens?.borderRadiusContainer as string) || '12px'), + fontSizeHeadingXl: extractNumericValue((themeCoreConfig.tokens?.fontSizeHeadingXl as string) || '26px'), + fontSizeHeadingL: extractNumericValue((themeCoreConfig.tokens?.fontSizeHeadingL as string) || '22px'), + fontSizeHeadingM: extractNumericValue((themeCoreConfig.tokens?.fontSizeHeadingM as string) || '20px'), + fontSizeHeadingS: extractNumericValue((themeCoreConfig.tokens?.fontSizeHeadingS as string) || '18px'), + fontSizeHeadingXs: extractNumericValue((themeCoreConfig.tokens?.fontSizeHeadingXs as string) || '16px'), + lineHeightHeadingXl: extractNumericValue((themeCoreConfig.tokens?.lineHeightHeadingXl as string) || '32px'), + lineHeightHeadingL: extractNumericValue((themeCoreConfig.tokens?.lineHeightHeadingL as string) || '26px'), + lineHeightHeadingM: extractNumericValue((themeCoreConfig.tokens?.lineHeightHeadingM as string) || '24px'), + lineHeightHeadingS: extractNumericValue((themeCoreConfig.tokens?.lineHeightHeadingS as string) || '22px'), + lineHeightHeadingXs: extractNumericValue((themeCoreConfig.tokens?.lineHeightHeadingXs as string) || '20px'), + fontFamilyBase: (themeCoreConfig.tokens?.fontFamilyBase as string) || '', + }); + setErrors({}); + // Reset to Cloudscape defaults by passing undefined + applyCustomTheme(undefined); + }; + + return ( + + + + + Theme Selection + + setConsoleTheme(detail.checked)} checked={consoleTheme}> + Console + + + + + Font + + + + handleInputChange('colorSelectedAccent', detail.value)} + /> + + + { + setCustomLinkColor(detail.checked); + if (detail.checked) { + // Set default to the second option when toggled on + handleInputChange( + 'colorTextLinkDefault', + `light: '${colorTextLinkSecondOption.light}', dark: '${colorTextLinkSecondOption.dark}'`, + ); + handleInputChange('colorTextLinkHover', "light: '#0033CC', dark: '#C2D1FF'"); + } else { + // Clear custom values when toggled off (will fall back to theme default) + handleInputChange('colorTextLinkDefault', ''); + handleInputChange('colorTextLinkHover', ''); + } + }} + checked={customLinkColor} + > + Use alternate link color + + + {customLinkColor && ( + <> + + handleInputChange('colorTextLinkDefault', detail.value)} + /> + + + handleInputChange('colorTextLinkHover', detail.value)} + /> + + + )} + + + + + + handleInputChange('borderWidthButton', detail.value)} + /> + + + + handleInputChange('borderRadiusButton', detail.value)} + /> + + + + handleInputChange('borderRadiusContainer', detail.value)} + /> + + + + handleInputChange('borderRadiusInput', detail.value)} + /> + + + + + + + Icon stroke + + + + + handleInputChange('borderWidthIconSmall', detail.value)} + /> + + + + handleInputChange('borderWidthIconNormal', detail.value)} + /> + + + + handleInputChange('borderWidthIconMedium', detail.value)} + /> + + + + handleInputChange('borderWidthIconBig', detail.value)} + /> + + + + handleInputChange('borderWidthIconLarge', detail.value)} + /> + + + + + + + Font related themes + + + + + H1 + + + handleInputChange('fontSizeHeadingXl', detail.value)} + /> + + + + handleInputChange('lineHeightHeadingXl', detail.value)} + /> + + + + H2 + + + handleInputChange('fontSizeHeadingL', detail.value)} + /> + + + + handleInputChange('lineHeightHeadingL', detail.value)} + /> + + + + H3 + + + handleInputChange('fontSizeHeadingM', detail.value)} + /> + + + + handleInputChange('lineHeightHeadingM', detail.value)} + /> + + + + H4 + + + handleInputChange('fontSizeHeadingS', detail.value)} + /> + + + + handleInputChange('lineHeightHeadingS', detail.value)} + /> + + + + H5 + + + handleInputChange('fontSizeHeadingXs', detail.value)} + /> + + + + handleInputChange('lineHeightHeadingXs', detail.value)} + /> + + + + + {/* + + { + setChecked(detail.checked); + }} + checked={checked} + > + Filled flashbar + + + */} + + + + + + + + + ); +} diff --git a/src/pages/commons/top-navigation.tsx b/src/pages/commons/top-navigation.tsx new file mode 100644 index 00000000..126660a5 --- /dev/null +++ b/src/pages/commons/top-navigation.tsx @@ -0,0 +1,91 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React, { ReactNode } from 'react'; +import { createPortal } from 'react-dom'; + +import { ButtonDropdownProps } from '@cloudscape-design/components/button-dropdown'; +import TopNavigation from '@cloudscape-design/components/top-navigation'; +import { TopNavigationProps } from '@cloudscape-design/components/top-navigation'; +import { Mode } from '@cloudscape-design/global-styles'; + +import { updateDensity, updateDirection, updateMode } from '../../common/apply-mode'; +import logo from '../../common/logo.svg'; + +/** + * This Portal is for demo purposes only due to the additional + * header used on the Demo page. + */ +interface DemoHeaderPortalProps { + children: ReactNode; +} + +const DemoHeaderPortal = ({ children }: DemoHeaderPortalProps) => { + const domNode = document.querySelector('#h')!; + return createPortal(children, domNode); +}; + +export function DemoTopNavigation() { + const handlePreferenceChange = (event: CustomEvent) => { + const itemId = event.detail.id; + + // Handle mode changes + if (itemId === Mode.Light || itemId === Mode.Dark) { + updateMode(itemId); + } + // Handle density changes + else if (itemId === 'comfortable' || itemId === 'compact') { + updateDensity(itemId); + } + // Handle direction changes + else if (itemId === 'ltr' || itemId === 'rtl') { + updateDirection(itemId); + } + }; + + const utilities: TopNavigationProps.Utility[] = [ + { + type: 'menu-dropdown', + text: 'Preferences', + items: [ + { + text: 'Appearance', + items: [ + { text: 'Light', id: Mode.Light }, + { text: 'Dark', id: Mode.Dark }, + ], + }, + { + text: 'Directionality', + items: [ + { text: 'Left-to-right', id: 'ltr' }, + { text: 'Right-to-left', id: 'rtl' }, + ], + }, + { + text: 'Density', + items: [ + { text: 'Comfortable', id: 'comfortable' }, + { text: 'Compact', id: 'compact' }, + ], + }, + ], + onItemClick: handlePreferenceChange, + }, + ]; + + return ( + + + + ); +} diff --git a/src/pages/commons/use-global-split-panel.ts b/src/pages/commons/use-global-split-panel.ts new file mode 100644 index 00000000..28461acd --- /dev/null +++ b/src/pages/commons/use-global-split-panel.ts @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import { useState } from 'react'; + +import { AppLayoutProps } from '@cloudscape-design/components/app-layout'; + +export const useGlobalSplitPanel = () => { + const [splitPanelSize, setSplitPanelSize] = useState(400); + const [splitPanelOpen, setSplitPanelOpen] = useState(false); + + const onSplitPanelResize: AppLayoutProps['onSplitPanelResize'] = ({ detail: { size } }) => { + setSplitPanelSize(size); + }; + + const onSplitPanelToggle: AppLayoutProps['onSplitPanelToggle'] = ({ detail: { open } }) => { + setSplitPanelOpen(open); + }; + + return { + splitPanelOpen, + onSplitPanelToggle, + splitPanelSize, + onSplitPanelResize, + splitPanelPreferences: { position: 'side' as const }, + }; +}; diff --git a/src/pages/commons/with-global-drawer.tsx b/src/pages/commons/with-global-drawer.tsx new file mode 100644 index 00000000..65fb0711 --- /dev/null +++ b/src/pages/commons/with-global-drawer.tsx @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React, { ReactNode, useEffect, useRef } from 'react'; + +import { registerGlobalDrawer } from './global-drawer-plugin'; + +interface WithGlobalDrawerProps { + children: ReactNode; +} + +export function WithGlobalDrawer({ children }: WithGlobalDrawerProps) { + const registered = useRef(false); + + useEffect(() => { + if (!registered.current) { + registerGlobalDrawer(); + registered.current = true; + } + }, []); + + return <>{children}; +} diff --git a/src/pages/components-overview/buttons-inputs-dropdowns.tsx b/src/pages/components-overview/buttons-inputs-dropdowns.tsx new file mode 100644 index 00000000..918ccb98 --- /dev/null +++ b/src/pages/components-overview/buttons-inputs-dropdowns.tsx @@ -0,0 +1,243 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React, { useState } from 'react'; + +import Button from '@cloudscape-design/components/button'; +import ButtonGroup from '@cloudscape-design/components/button-group'; +import Calendar from '@cloudscape-design/components/calendar'; +import DatePicker from '@cloudscape-design/components/date-picker'; +import ExpandableSection from '@cloudscape-design/components/expandable-section'; +import Grid from '@cloudscape-design/components/grid'; +import Multiselect, { MultiselectProps } from '@cloudscape-design/components/multiselect'; +import SegmentedControl from '@cloudscape-design/components/segmented-control'; +import SpaceBetween from '@cloudscape-design/components/space-between'; +import ToggleButton from '@cloudscape-design/components/toggle-button'; + +import { generateDropdownOptions } from './component-data'; +import { Section } from './utils'; + +function Buttons() { + const [selectedSegment, setSelectedSegment] = useState('seg-1'); + const [toggle1, setToggle1] = useState(true); + const [toggle2, setToggle2] = useState(false); + const [toggle3, setToggle3] = useState(false); + const [toggle4, setToggle4] = useState(true); + const [toggle5, setToggle5] = useState(false); + const [toggle6, setToggle6] = useState(true); + + return ( + + + + + + + + + + + + + + +); + +const barChartDates = [ + new Date(1601071200000), + new Date(1601078400000), + new Date(1601085600000), + new Date(1601092800000), + new Date(1601100000000), +]; + +const lineChartDomain: [Date, Date] = [new Date(1600984800000), new Date(1601013600000)]; + +const lineChartSite1 = [ + { x: new Date(1600984800000), y: 58020 }, + { x: new Date(1600985700000), y: 102402 }, + { x: new Date(1600986600000), y: 104920 }, + { x: new Date(1600987500000), y: 94031 }, + { x: new Date(1600988400000), y: 125021 }, + { x: new Date(1600989300000), y: 159219 }, + { x: new Date(1600990200000), y: 193082 }, + { x: new Date(1600991100000), y: 162592 }, + { x: new Date(1600992000000), y: 274021 }, + { x: new Date(1600992900000), y: 264286 }, + { x: new Date(1600993800000), y: 289210 }, + { x: new Date(1600994700000), y: 256362 }, + { x: new Date(1600995600000), y: 257306 }, + { x: new Date(1600996500000), y: 186776 }, + { x: new Date(1600997400000), y: 294020 }, + { x: new Date(1600998300000), y: 385975 }, + { x: new Date(1600999200000), y: 486039 }, + { x: new Date(1601000100000), y: 490447 }, + { x: new Date(1601001000000), y: 361845 }, + { x: new Date(1601001900000), y: 339058 }, + { x: new Date(1601002800000), y: 298028 }, + { x: new Date(1601003700000), y: 231902 }, + { x: new Date(1601004600000), y: 224558 }, + { x: new Date(1601005500000), y: 253901 }, + { x: new Date(1601006400000), y: 102839 }, + { x: new Date(1601007300000), y: 234943 }, + { x: new Date(1601008200000), y: 204405 }, + { x: new Date(1601009100000), y: 190391 }, + { x: new Date(1601010000000), y: 183570 }, + { x: new Date(1601010900000), y: 162592 }, + { x: new Date(1601011800000), y: 148910 }, + { x: new Date(1601012700000), y: 229492 }, + { x: new Date(1601013600000), y: 293910 }, +]; + +const lineChartSite2 = [ + { x: new Date(1600984800000), y: 151023 }, + { x: new Date(1600985700000), y: 169975 }, + { x: new Date(1600986600000), y: 176980 }, + { x: new Date(1600987500000), y: 168852 }, + { x: new Date(1600988400000), y: 149130 }, + { x: new Date(1600989300000), y: 147299 }, + { x: new Date(1600990200000), y: 169552 }, + { x: new Date(1600991100000), y: 163401 }, + { x: new Date(1600992000000), y: 154091 }, + { x: new Date(1600992900000), y: 199516 }, + { x: new Date(1600993800000), y: 195503 }, + { x: new Date(1600994700000), y: 189953 }, + { x: new Date(1600995600000), y: 181635 }, + { x: new Date(1600996500000), y: 192975 }, + { x: new Date(1600997400000), y: 205951 }, + { x: new Date(1600998300000), y: 218958 }, + { x: new Date(1600999200000), y: 220516 }, + { x: new Date(1601000100000), y: 213557 }, + { x: new Date(1601001000000), y: 165899 }, + { x: new Date(1601001900000), y: 173557 }, + { x: new Date(1601002800000), y: 172331 }, + { x: new Date(1601003700000), y: 186492 }, + { x: new Date(1601004600000), y: 131541 }, + { x: new Date(1601005500000), y: 142262 }, + { x: new Date(1601006400000), y: 194091 }, + { x: new Date(1601007300000), y: 185899 }, + { x: new Date(1601008200000), y: 173401 }, + { x: new Date(1601009100000), y: 171635 }, + { x: new Date(1601010000000), y: 179130 }, + { x: new Date(1601010900000), y: 185951 }, + { x: new Date(1601011800000), y: 144091 }, + { x: new Date(1601012700000), y: 152975 }, + { x: new Date(1601013600000), y: 157299 }, +]; + +export default function Charts() { + return ( +
+ <> + + + ({ x, y: [12, 18, 15, 9, 18][i] })), + }, + { + title: 'Moderate', + type: 'bar', + data: barChartDates.map((x, i) => ({ x, y: [8, 11, 12, 11, 13][i] })), + }, + { + title: 'Low', + type: 'bar', + data: barChartDates.map((x, i) => ({ x, y: [7, 9, 8, 7, 5][i] })), + }, + { + title: 'Unclassified', + type: 'bar', + data: barChartDates.map((x, i) => ({ x, y: [14, 8, 6, 4, 6][i] })), + }, + ]} + xDomain={barChartDates} + yDomain={[0, 50]} + i18nStrings={{ xTickFormatter: formatDateTick }} + ariaLabel="Stacked bar chart" + height={300} + stackedBars={true} + xTitle="Time (UTC)" + yTitle="Error count" + empty={emptyState} + noMatch={noMatchState} + /> + + + + + + + + [ + { key: 'Resource count', value: datum.value }, + { key: 'Percentage', value: `${((datum.value / sum) * 100).toFixed(0)}%` }, + { key: 'Last update on', value: datum.lastUpdate }, + ]} + segmentDescription={(datum, sum) => `${datum.value} units, ${((datum.value / sum) * 100).toFixed(0)}%`} + ariaDescription="Pie chart showing how many resources are currently in which state." + ariaLabel="Pie chart" + empty={emptyState} + noMatch={noMatchState} + /> + + + +
+ ); +} diff --git a/src/pages/components-overview/chat.tsx b/src/pages/components-overview/chat.tsx new file mode 100644 index 00000000..cbb8b054 --- /dev/null +++ b/src/pages/components-overview/chat.tsx @@ -0,0 +1,136 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React, { useState } from 'react'; + +import Avatar from '@cloudscape-design/chat-components/avatar'; +import ChatBubble from '@cloudscape-design/chat-components/chat-bubble'; +import SupportPromptGroup from '@cloudscape-design/chat-components/support-prompt-group'; +import Box from '@cloudscape-design/components/box'; +import ButtonGroup from '@cloudscape-design/components/button-group'; +import FileTokenGroup from '@cloudscape-design/components/file-token-group'; +import Grid from '@cloudscape-design/components/grid'; +import PromptInput from '@cloudscape-design/components/prompt-input'; +import SpaceBetween from '@cloudscape-design/components/space-between'; + +import { Section } from './utils'; + +export default function Chat() { + const [value, setValue] = useState(''); + const [files, setFiles] = React.useState([ + new File([new Blob(['Test content'])], 'file-1.pdf', { + type: 'application/pdf', + lastModified: 1590962400000, + }), + new File([new Blob(['Test content'])], 'file-2.pdf', { + type: 'application/pdf', + lastModified: 1590962400000, + }), + ]); + return ( +
+ + + } + ariaLabel="Jane Doe message" + type="outgoing" + > + This is an outgoing message from a user. + + } + ariaLabel="AI Assistant message" + type="incoming" + > + This is an incoming message from the AI assistant. + + + } + > + Generating response + + + + + { + /* no-op for demo purposes */ + }} + /> + + + setValue(detail.value)} + value={value} + actionButtonAriaLabel="Send message" + actionButtonIconName="send" + disableSecondaryActionsPaddings + placeholder="Ask a question" + ariaLabel="Prompt input with files" + secondaryActions={ + + + + } + secondaryContent={ + ({ file }))} + onDismiss={({ detail }) => setFiles(files => files.filter((_, index) => index !== detail.fileIndex))} + alignment="horizontal" + showFileSize={true} + showFileLastModified={true} + showFileThumbnail={true} + i18nStrings={{ + removeFileAriaLabel: () => 'Remove file', + limitShowFewer: 'Show fewer files', + limitShowMore: 'Show more files', + errorIconAriaLabel: 'Error', + warningIconAriaLabel: 'Warning', + }} + /> + } + /> + +
+ ); +} diff --git a/src/pages/components-overview/component-data.tsx b/src/pages/components-overview/component-data.tsx new file mode 100644 index 00000000..21e8a662 --- /dev/null +++ b/src/pages/components-overview/component-data.tsx @@ -0,0 +1,289 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React from 'react'; +import padStart from 'lodash/padStart'; +import range from 'lodash/range'; + +import { BoxProps } from '@cloudscape-design/components/box'; +import { FlashbarProps } from '@cloudscape-design/components/flashbar'; +import { MultiselectProps } from '@cloudscape-design/components/multiselect'; +import ProgressBar from '@cloudscape-design/components/progress-bar'; +import { SelectProps } from '@cloudscape-design/components/select'; +import { StatusIndicatorProps } from '@cloudscape-design/components/status-indicator'; + +let seed = 1; +export default function pseudoRandom() { + const x = Math.sin(seed++) * 10000; + return x - Math.floor(x); +} + +export const items = [ + { id: '1', title: 'Item 1', description: 'Description 1' }, + { id: '2', title: 'Item 2', description: 'Description 2' }, + { id: '3', title: 'Item 3', description: 'Description 3' }, +]; + +export type InstanceState = 'PENDING' | 'RUNNING' | 'STOPPING' | 'STOPPED' | 'TERMINATING' | 'TERMINATED'; + +export interface Instance { + id: string; + name: string; + description: string; + state: InstanceState; + type: string; + imageId: string; + dnsName?: string; +} + +export function id() { + const id = Math.ceil(pseudoRandom() * Math.pow(16, 8)).toString(16); + return padStart(id, 8, '0'); +} + +function state() { + const states = [ + 'PENDING', + 'RUNNING', + 'RUNNING', + 'RUNNING', + 'STOPPING', + 'STOPPED', + 'STOPPED', + 'TERMINATED', + 'TERMINATING', + ] as const; + return states[Math.floor(pseudoRandom() * states.length)]; +} + +function number() { + return 1 + Math.floor(pseudoRandom() * 256); +} + +function dnsName() { + return `ec2-${number()}-${number()}-${number()}-${number()}.eu-west-1.compute.amazonaws.com`; +} + +export const flashbarItems: FlashbarProps.MessageDefinition[] = [ + { + header: 'Success', + type: 'success', + content: 'This is a success message -- check it out!', + dismissible: true, + dismissLabel: 'Dismiss success message', + id: 'success', + }, + { + header: 'Warning', + type: 'warning', + content: 'This is a warning message -- check it out!', + dismissible: true, + dismissLabel: 'Dismiss warning message', + id: 'warning', + }, + { + header: 'Error', + type: 'error', + content: 'This is an error message -- check it out!', + dismissible: true, + dismissLabel: 'Dismiss error message', + id: 'error', + }, + { + header: 'Info', + type: 'info', + content: 'This is an info message -- check it out!', + dismissible: true, + dismissLabel: 'Dismiss info message', + id: 'info', + }, + { + header: 'In-progress', + type: 'in-progress', + content: ( + <> + This is an in-progress flash -- check it out! + + + ), + dismissible: true, + dismissLabel: 'Dismiss in-progress message', + id: 'in-progress', + }, + { + header: 'Loading', + type: 'in-progress', + loading: true, + content: 'This is a loading flash -- check it out!', + dismissible: true, + dismissLabel: 'Dismiss loading message', + id: 'loading', + }, +]; + +export interface RandomData { + description: string; + name: string; + amount: string; + increase: boolean; +} + +const collectionData: RandomData[] = [ + { + description: 'volutpat. Nulla dignissim. Maecenas ornare egestas ligula. Nullam feugiat placerat', + name: 'Velit Egestas LLP', + amount: '$68.54', + increase: true, + }, + { + description: 'vestibulum lorem, sit amet ultricies sem magna nec quam. Curabitur', + name: 'Mattis Velit Justo Company', + amount: '$80.38', + increase: true, + }, + { + description: 'aliquet odio. Etiam ligula tortor, dictum eu, placerat eget, venenatis', + name: 'Tempor LLP', + amount: '$1.66', + increase: false, + }, + { + description: 'ridiculus mus. Donec dignissim magna a tortor. Nunc commodo auctor', + name: 'Egestas Hendrerit Neque Corporation', + amount: '$31.74', + increase: true, + }, + { + description: 'Vivamus molestie dapibus ligula. Aliquam erat volutpat. Nulla dignissim. Maecenas', + name: 'Aenean Incorporated', + amount: '$53.61', + increase: true, + }, + { + description: 'Cras sed leo. Cras vehicula aliquet libero. Integer in magna.', + name: 'Proin Ltd', + amount: '$42.19', + increase: false, + }, + { + description: 'Phasellus at augue id ante dictum cursus. Nunc mauris elit,', + name: 'Nulla Facilisi Foundation', + amount: '$97.03', + increase: true, + }, + { + description: 'Sed nec metus facilisis lorem tristique aliquet. Phasellus fermentum convallis', + name: 'Donec Vitae Corp', + amount: '$15.88', + increase: false, + }, +]; + +export const cardItems = collectionData.slice(0, 2); +export const tableItems = collectionData; + +export const fontSizes: BoxProps.FontSize[] = [ + 'body-s', + 'body-m', + 'heading-xs', + 'heading-s', + 'heading-m', + 'heading-l', + 'heading-xl', + 'display-l', +]; + +export const statusToText: [StatusIndicatorProps.Type, string][] = [ + ['error', 'Error'], + ['warning', 'Warning'], + ['success', 'Success'], + ['info', 'Info'], + ['stopped', 'Stopped'], + ['pending', 'Pending'], + ['in-progress', 'In progress'], + ['loading', 'Loading'], +]; + +export function instanceType() { + const types = [ + 't1.micro', + 't2.nano', + 't2.small', + 't2.xlarge', + 't2.2xlarge', + 'm3.medium', + 'm3.large', + 'm3.xlarge', + 'm3.2xlarge', + 'm4.large', + 'm4.xlarge', + 'm4.2xlarge', + 'm4.4xlarge', + 'm4.10xlarge', + 'm4.16xlarge', + 'cr1.8xlarge', + 'r5.large', + 'r5.xlarge', + 'r5.2xlarge', + 'r5.metal', + 'r5d.xlarge', + 'r5d.2xlarge', + 'r5d.4xlarge', + 'r5d.8xlarge', + 'r5d.12xlarge', + 'r5d.16xlarge', + 'r5d.24xlarge', + 'r5d.metal', + 'i3.large', + 'i3.xlarge', + 'i3.2xlarge', + 'i3.16xlarge', + 'c3.large', + 'c3.xlarge', + 'c4.2xlarge', + 'c5.large', + 'c5.4xlarge ', + 'g2.2xlarge', + 'p2.xlarge', + 'm5.large', + 'm5.xlarge', + 'm5.2xlarge', + 'u-6tb1.metal', + ]; + return types[Math.floor(pseudoRandom() * types.length)]; +} + +function imageId() { + return `ami-${id()}`; +} + +export function generateCollectionItems(count = 5): Instance[] { + return range(count).map(() => { + const value: Instance = { + id: id(), + name: `Instance ${id()}`, + description: '', + state: state(), + type: instanceType(), + imageId: imageId(), + }; + if (value.state !== 'PENDING') { + value.dnsName = dnsName(); + } + return value; + }); +} + +export const generateDropdownOptions = (count = 25): SelectProps.Options | MultiselectProps.Options => { + return [...Array(count).keys()].map(n => { + const numberToDisplay = (n + 1).toString(); + const baseOption = { + id: numberToDisplay, + value: numberToDisplay, + label: `Option ${numberToDisplay}`, + }; + if (n === 0 || n === 24 || n === 49) { + return { ...baseOption, disabled: true, disabledReason: 'disabled reason' }; + } + return baseOption; + }); +}; diff --git a/src/pages/components-overview/form-controls.tsx b/src/pages/components-overview/form-controls.tsx new file mode 100644 index 00000000..c93547b3 --- /dev/null +++ b/src/pages/components-overview/form-controls.tsx @@ -0,0 +1,184 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React from 'react'; + +import Checkbox from '@cloudscape-design/components/checkbox'; +import ColumnLayout from '@cloudscape-design/components/column-layout'; +import Grid from '@cloudscape-design/components/grid'; +import RadioGroup from '@cloudscape-design/components/radio-group'; +import Slider from '@cloudscape-design/components/slider'; +import SpaceBetween from '@cloudscape-design/components/space-between'; +import Tiles from '@cloudscape-design/components/tiles'; +import Toggle from '@cloudscape-design/components/toggle'; + +import { Section } from './utils'; + +export default function FormControls() { + return ( +
+ + + + + Checked + + + Unchecked + + + Disabled + + + Disabled + + + Read-only + + + Read-only + + + Read-only, disabled + + + Read-only, disabled + + + + + + + + + + + + + Checked + + + Unchecked + + + Disabled + + + Disabled + + + Read-only + + + Read-only + + + Read-only, disabled + + + Read-only, disabled + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/pages/components-overview/images.tsx b/src/pages/components-overview/images.tsx new file mode 100644 index 00000000..0e111db0 --- /dev/null +++ b/src/pages/components-overview/images.tsx @@ -0,0 +1,193 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React from 'react'; + +import Box from '@cloudscape-design/components/box'; +import Button from '@cloudscape-design/components/button'; +import ColumnLayout from '@cloudscape-design/components/column-layout'; +import Container from '@cloudscape-design/components/container'; +import Header from '@cloudscape-design/components/header'; +import Link from '@cloudscape-design/components/link'; +import SpaceBetween from '@cloudscape-design/components/space-between'; + +import imageExampleA from '../../common/image-example-1.png'; +import imageExampleB from '../../common/image-example-2.png'; +import imageExampleC from '../../common/image-example-3.png'; + +export default function Images() { + return ( + +
Container with media
+ + , + position: 'side', + width: '40%', + }} + > + + + + + Product title + + + Company name + + + This is a paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus tempor dolor ac + accumsan. + + + $0.1/hour + + + + + + , + position: 'side', + width: '40%', + }} + > + + + + + Product title + + + Company name + + + This is a paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus tempor dolor ac + accumsan. + + + $0.1/hour + + + + + + , + position: 'side', + width: '40%', + }} + > + + + + + Product title + + + Company name + + + This is a paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus tempor dolor ac + accumsan. + + + $0.1/hour + + + + + + + + Video thumbnail + + ), + height: 200, + }} + > + + + 43 min + + + Video Title + + + + This is a paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus tempor dolor ac + accumsan. This is a paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus tempor + dolor ac accumsan. + + + + + Video thumbnail + + ), + height: 200, + }} + > + + + 43 min + + + Video Title + + + + This is a paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus tempor dolor ac + accumsan. This is a paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus tempor + dolor ac accumsan. + + + + + Video thumbnail + + ), + height: 200, + }} + > + + + 43 min + + + Video Title + + + + This is a paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus tempor dolor ac + accumsan. This is a paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus tempor + dolor ac accumsan. + + + +
+ ); +} diff --git a/src/pages/components-overview/index.tsx b/src/pages/components-overview/index.tsx new file mode 100644 index 00000000..b06a7613 --- /dev/null +++ b/src/pages/components-overview/index.tsx @@ -0,0 +1,99 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React from 'react'; +import { createRoot } from 'react-dom/client'; + +import { applyTheme } from '@cloudscape-design/components/theming'; + +import { themeCoreConfig } from '../../common/theme-core'; +applyTheme({ theme: themeCoreConfig }); + +import Header from '@cloudscape-design/components/header'; +import SpaceBetween from '@cloudscape-design/components/space-between'; +import SplitPanel from '@cloudscape-design/components/split-panel'; + +import { Notifications } from '../commons'; +import { + CustomAppLayout, + DemoTopNavigation, + GlobalSplitPanelContent, + useGlobalSplitPanel, +} from '../commons/common-components'; +import ButtonsInputsDropdowns from './buttons-inputs-dropdowns'; +import Chat from './chat'; +import FormControls from './form-controls'; +import Images from './images'; +import KvpForm from './kvp-form'; +import NavigationComponents from './navigation-components'; +import StatusComponents from './status-components'; +import TableAndCards from './table-and-cards'; +import Typography from './typography'; + +import '../../styles/base.scss'; +import '../../styles/top-navigation.scss'; + +function App() { + const { splitPanelOpen, onSplitPanelToggle, splitPanelSize, onSplitPanelResize, splitPanelPreferences } = + useGlobalSplitPanel(); + + return ( + <> + + } + splitPanelOpen={splitPanelOpen} + onSplitPanelToggle={onSplitPanelToggle} + splitPanelSize={splitPanelSize} + onSplitPanelResize={onSplitPanelResize} + splitPanelPreferences={splitPanelPreferences} + splitPanel={ + + + + } + content={ + +
+ Components overview page +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ } + /> + + ); +} + +createRoot(document.getElementById('app')!).render(); diff --git a/src/pages/components-overview/kvp-form.tsx b/src/pages/components-overview/kvp-form.tsx new file mode 100644 index 00000000..aa3f7bd7 --- /dev/null +++ b/src/pages/components-overview/kvp-form.tsx @@ -0,0 +1,144 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import React from 'react'; + +import { ColumnLayout } from '@cloudscape-design/components'; +import Box from '@cloudscape-design/components/box'; +import Button from '@cloudscape-design/components/button'; +import CopyToClipboard from '@cloudscape-design/components/copy-to-clipboard'; +import Form from '@cloudscape-design/components/form'; +import FormField from '@cloudscape-design/components/form-field'; +import Grid from '@cloudscape-design/components/grid'; +import Header from '@cloudscape-design/components/header'; +import Icon from '@cloudscape-design/components/icon'; +import Input from '@cloudscape-design/components/input'; +import KeyValuePairs from '@cloudscape-design/components/key-value-pairs'; +import Link from '@cloudscape-design/components/link'; +import Select from '@cloudscape-design/components/select'; +import SpaceBetween from '@cloudscape-design/components/space-between'; +import StatusIndicator from '@cloudscape-design/components/status-indicator'; +import Textarea from '@cloudscape-design/components/textarea'; +import Tiles from '@cloudscape-design/components/tiles'; + +import { Section } from './utils'; + +export default function KvpForm() { + const [value, setValue] = React.useState('standard'); + return ( +
+ + + +
General configuration
+ + Info + + ), + }, + { + label: 'Status', + value: Available, + }, + { label: 'Price class', value: 'Use only US, Canada, Europe, and Asia' }, + { label: 'CNAMEs', value: example.com }, + { + label: 'ARN', + value: ( + + ), + }, + ]} + /> +
+
+ + +
+ + + + } + header={
Create instance
} + > + + + + + setValue(e.detail.value)} + columns={4} + items={[ + { + value: 'standard', + label: 'Standard', + description: 'Recommended for most workloads', + image: , + }, + { + value: 'optimized', + label: 'Optimized', + description: 'Best for dynamic content', + image: , + }, + { + value: 'custom', + label: 'Custom', + description: 'Configure your own policy', + image: , + }, + { + value: 'disabled', + label: 'Disabled', + description: 'No caching applied', + image: , + }, + ]} + ariaLabel="Cache policy" + /> + + + + + + + + +