├── .gitignore ├── .gitmodules ├── vite.config.js ├── client ├── example-app │ ├── README.md │ ├── example-app.css │ ├── index.html │ ├── help-content.html │ └── example-app.js ├── index.html ├── app.js ├── help-content-template.html ├── help-modal.js └── bespoke-template.css ├── package.json ├── .github └── workflows │ └── build-release.yml ├── LICENSE ├── AGENTS.md ├── server.js ├── README.md └── BESPOKE-TEMPLATE.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "client/design-system"] 2 | path = client/design-system 3 | url = https://github.com/CodeSignal/learn_bespoke-design-system.git 4 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | root: './client', 5 | server: { 6 | host: '0.0.0.0', 7 | hmr: true, 8 | allowedHosts: true, 9 | port: 3000, 10 | proxy: { 11 | '/message': { 12 | target: 'http://localhost:3001', 13 | changeOrigin: true 14 | }, 15 | '/ws': { 16 | target: 'ws://localhost:3001', 17 | ws: true, 18 | changeOrigin: true 19 | } 20 | } 21 | }, 22 | build: { 23 | outDir: '../dist', 24 | emptyOutDir: true 25 | } 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /client/example-app/README.md: -------------------------------------------------------------------------------- 1 | # Example App 2 | 3 | This directory contains an example application that demonstrates how to use the Bespoke Simulation template and its design system components. The example app showcases a simple interactive counter application that uses buttons, inputs, dropdowns, tags, and other design system components to illustrate the template's features and usage patterns. Accessible via the development server at `http://localhost:3000/example-app/index.html`. 4 | 5 | **Important:** This example app is included for reference and testing purposes only. When customizing this template for your own application, you should remove this entire `example-app` directory and replace it with your own application code: 6 | 7 | ```bash 8 | rm -rf client/example-app 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bespoke-template", 3 | "version": "0.0.2", 4 | "description": "Bespoke template with local development server and WebSocket messaging", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "npm run start:prod", 8 | "start:prod": "IS_PRODUCTION=true node server.js", 9 | "start:dev": "concurrently \"npm run dev:vite\" \"npm run dev:api\"", 10 | "dev:vite": "vite", 11 | "dev:api": "PORT=3001 node server.js", 12 | "build": "vite build" 13 | }, 14 | "keywords": [ 15 | "bespoke", 16 | "template", 17 | "development", 18 | "server", 19 | "websocket" 20 | ], 21 | "author": "", 22 | "license": "MIT", 23 | "dependencies": { 24 | "ws": "^8.14.2" 25 | }, 26 | "devDependencies": { 27 | "concurrently": "^8.2.2", 28 | "vite": "^7.2.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | build-and-release: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Populate design system submodule 19 | run: git submodule update --init 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: '22.13.1' 25 | cache: 'npm' 26 | 27 | - name: Install all dependencies 28 | run: npm ci 29 | 30 | - name: Build project 31 | run: npm run build 32 | 33 | - name: Install production dependencies only 34 | run: | 35 | npm ci --production 36 | 37 | - name: Create release tarball 38 | run: | 39 | tar -czf release.tar.gz dist/ package.json server.js node_modules/ 40 | 41 | - name: Upload build artifact (for workflow logs) 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: dist 45 | path: dist 46 | 47 | - name: Create GitHub Release and upload asset 48 | uses: ncipollo/release-action@v1 49 | with: 50 | token: ${{ secrets.GITHUB_TOKEN }} 51 | tag: v${{ github.run_number }} 52 | name: Release ${{ github.run_number }} 53 | body: | 54 | Latest build from main branch. 55 | artifacts: release.tar.gz 56 | allowUpdates: false 57 | draft: false 58 | prerelease: false 59 | makeLatest: true 60 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <!-- APP_TITLE --> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |

APP NAME

32 | 33 |
34 |
Ready
35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | // app.js 2 | (function() { 3 | const status = document.getElementById('status'); 4 | let websocket = null; 5 | 6 | function setStatus(msg) { 7 | status.textContent = msg; 8 | } 9 | 10 | // Initialize WebSocket connection 11 | function initializeWebSocket() { 12 | const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 13 | const host = window.location.host; 14 | const wsUrl = `${protocol}//${host}/ws`; 15 | 16 | try { 17 | websocket = new WebSocket(wsUrl); 18 | 19 | websocket.onopen = function(event) { 20 | console.log('WebSocket connected'); 21 | setStatus('Ready (WebSocket connected)'); 22 | }; 23 | 24 | websocket.onmessage = function(event) { 25 | try { 26 | const data = JSON.parse(event.data); 27 | if (data.type === 'message' && data.message) { 28 | alert(data.message); 29 | } 30 | } catch (error) { 31 | console.error('Error parsing WebSocket message:', error); 32 | } 33 | }; 34 | 35 | websocket.onclose = function(event) { 36 | console.log('WebSocket disconnected'); 37 | setStatus('Ready (WebSocket disconnected)'); 38 | 39 | // Attempt to reconnect after 3 seconds 40 | setTimeout(() => { 41 | console.log('Attempting to reconnect WebSocket...'); 42 | initializeWebSocket(); 43 | }, 3000); 44 | }; 45 | 46 | websocket.onerror = function(error) { 47 | console.error('WebSocket error:', error); 48 | setStatus('Ready (WebSocket error)'); 49 | }; 50 | 51 | } catch (error) { 52 | console.error('Failed to create WebSocket connection:', error); 53 | setStatus('Ready (WebSocket unavailable)'); 54 | } 55 | } 56 | 57 | // Load help content and initialize modal 58 | async function initializeHelpModal() { 59 | try { 60 | const response = await fetch('./help-content-template.html'); 61 | const helpContent = await response.text(); 62 | 63 | // Initialize help modal with actual content 64 | HelpModal.init({ 65 | triggerSelector: '#btn-help', 66 | content: helpContent, 67 | theme: 'auto' 68 | }); 69 | 70 | setStatus('Ready'); 71 | } catch (error) { 72 | console.error('Failed to load help content:', error); 73 | // Fallback to placeholder content 74 | HelpModal.init({ 75 | triggerSelector: '#btn-help', 76 | content: '

Help content could not be loaded. Please check that help-content-template.html exists.

', 77 | theme: 'auto' 78 | }); 79 | setStatus('Ready (help content unavailable)'); 80 | } 81 | } 82 | 83 | // Initialize both help modal and WebSocket when DOM is ready 84 | function initialize() { 85 | initializeHelpModal(); 86 | initializeWebSocket(); 87 | } 88 | 89 | if (document.readyState === 'loading') { 90 | document.addEventListener('DOMContentLoaded', initialize); 91 | } else { 92 | initialize(); 93 | } 94 | })(); 95 | -------------------------------------------------------------------------------- /client/example-app/example-app.css: -------------------------------------------------------------------------------- 1 | /* Example App Styles - Interactive Component Showcase */ 2 | 3 | .bespoke .sidebar { 4 | padding: var(--UI-Spacing-spacing-xl); 5 | overflow-y: auto; 6 | } 7 | 8 | .bespoke .sidebar-section { 9 | display: flex; 10 | flex-direction: column; 11 | gap: var(--UI-Spacing-spacing-xl); 12 | } 13 | 14 | .bespoke .sidebar-section h2 { 15 | font-size: var(--Fonts-Headlines-sm); 16 | font-weight: 600; 17 | color: var(--Colors-Text-Body-Strongest); 18 | margin: 0 0 var(--UI-Spacing-spacing-ml) 0; 19 | } 20 | 21 | .bespoke .control-group { 22 | display: flex; 23 | flex-direction: column; 24 | gap: var(--UI-Spacing-spacing-s); 25 | } 26 | 27 | .bespoke .control-group label { 28 | font-size: var(--Fonts-Body-Default-sm); 29 | font-weight: 500; 30 | color: var(--Colors-Text-Body-Strong); 31 | } 32 | 33 | .bespoke .control-group .button { 34 | width: 100%; 35 | } 36 | 37 | .bespoke .control-group-buttons { 38 | flex-direction: row; 39 | gap: var(--UI-Spacing-spacing-s); 40 | } 41 | 42 | .bespoke .control-group-buttons .button { 43 | flex: 1; 44 | } 45 | 46 | .bespoke .content-area { 47 | overflow-y: auto; 48 | padding: var(--UI-Spacing-spacing-xl); 49 | } 50 | 51 | .bespoke .display-container { 52 | max-width: 800px; 53 | margin: 0 auto; 54 | display: flex; 55 | flex-direction: column; 56 | gap: var(--UI-Spacing-spacing-xl); 57 | } 58 | 59 | .bespoke .counter-display { 60 | flex-direction: column; 61 | align-items: center; 62 | justify-content: center; 63 | padding: var(--UI-Spacing-spacing-2xl); 64 | text-align: center; 65 | } 66 | 67 | .bespoke .counter-display h2 { 68 | font-size: var(--Fonts-Headlines-sm); 69 | font-weight: 600; 70 | color: var(--Colors-Text-Body-Strong); 71 | margin: 0 0 var(--UI-Spacing-spacing-ml) 0; 72 | text-align: center; 73 | } 74 | 75 | .bespoke .counter-value { 76 | font-size: 4rem; 77 | font-weight: 700; 78 | font-family: var(--heading-family); 79 | color: var(--Colors-Text-Body-Strongest); 80 | line-height: 1; 81 | text-align: center; 82 | } 83 | 84 | .bespoke .tags-container { 85 | display: flex; 86 | gap: var(--UI-Spacing-spacing-ml); 87 | justify-content: center; 88 | flex-wrap: wrap; 89 | } 90 | 91 | .bespoke .settings-display { 92 | flex-direction: column; 93 | align-items: flex-start; 94 | justify-content: flex-start; 95 | padding: var(--UI-Spacing-spacing-xl); 96 | } 97 | 98 | .bespoke .settings-display h3 { 99 | font-size: var(--Fonts-Headlines-xs); 100 | font-weight: 600; 101 | color: var(--Colors-Text-Body-Strong); 102 | margin: 0 0 var(--UI-Spacing-spacing-ml) 0; 103 | width: 100%; 104 | } 105 | 106 | .bespoke .setting-item { 107 | font-size: var(--Fonts-Body-Default-sm); 108 | color: var(--Colors-Text-Body-Default); 109 | margin-bottom: var(--UI-Spacing-spacing-s); 110 | width: 100%; 111 | } 112 | 113 | .bespoke .setting-item:last-child { 114 | margin-bottom: 0; 115 | } 116 | 117 | .bespoke .setting-item strong { 118 | color: var(--Colors-Text-Body-Strong); 119 | margin-right: var(--UI-Spacing-spacing-xs); 120 | } 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Elastic License 2.0 2 | 3 | URL: https://www.elastic.co/licensing/elastic-license 4 | 5 | ## Acceptance 6 | 7 | By using the software, you agree to all of the terms and conditions below. 8 | 9 | ## Copyright License 10 | 11 | The licensor grants you a non-exclusive, royalty-free, worldwide, 12 | non-sublicensable, non-transferable license to use, copy, distribute, make 13 | available, and prepare derivative works of the software, in each case subject to 14 | the limitations and conditions below. 15 | 16 | ## Limitations 17 | 18 | You may not provide the software to third parties as a hosted or managed 19 | service, where the service provides users with access to any substantial set of 20 | the features or functionality of the software. 21 | 22 | You may not move, change, disable, or circumvent the license key functionality 23 | in the software, and you may not remove or obscure any functionality in the 24 | software that is protected by the license key. 25 | 26 | You may not alter, remove, or obscure any licensing, copyright, or other notices 27 | of the licensor in the software. Any use of the licensor’s trademarks is subject 28 | to applicable law. 29 | 30 | ## Patents 31 | 32 | The licensor grants you a license, under any patent claims the licensor can 33 | license, or becomes able to license, to make, have made, use, sell, offer for 34 | sale, import and have imported the software, in each case subject to the 35 | limitations and conditions in this license. This license does not cover any 36 | patent claims that you cause to be infringed by modifications or additions to 37 | the software. If you or your company make any written claim that the software 38 | infringes or contributes to infringement of any patent, your patent license for 39 | the software granted under these terms ends immediately. If your company makes 40 | such a claim, your patent license ends immediately for work on behalf of your 41 | company. 42 | 43 | ## Notices 44 | 45 | You must ensure that anyone who gets a copy of any part of the software from you 46 | also gets a copy of these terms. 47 | 48 | If you modify the software, you must include in any modified copies of the 49 | software prominent notices stating that you have modified the software. 50 | 51 | ## No Other Rights 52 | 53 | These terms do not imply any licenses other than those expressly granted in 54 | these terms. 55 | 56 | ## Termination 57 | 58 | If you use the software in violation of these terms, such use is not licensed, 59 | and your licenses will automatically terminate. If the licensor provides you 60 | with a notice of your violation, and you cease all violation of this license no 61 | later than 30 days after you receive that notice, your licenses will be 62 | reinstated retroactively. However, if you violate these terms after such 63 | reinstatement, any additional violation of these terms will cause your licenses 64 | to terminate automatically and permanently. 65 | 66 | ## No Liability 67 | 68 | *As far as the law allows, the software comes as is, without any warranty or 69 | condition, and the licensor will not be liable to you for any damages arising 70 | out of these terms or the use or nature of the software, under any kind of 71 | legal claim.* 72 | 73 | ## Definitions 74 | 75 | The **licensor** is the entity offering these terms, and the **software** is the 76 | software the licensor makes available under these terms, including any portion 77 | of it. 78 | 79 | **you** refers to the individual or entity agreeing to these terms. 80 | 81 | **your company** is any legal entity, sole proprietorship, or other kind of 82 | organization that you work for, plus all organizations that have control over, 83 | are under the control of, or are under common control with that 84 | organization. **control** means ownership of substantially all the assets of an 85 | entity, or the power to direct its management and policies by vote, contract, or 86 | otherwise. Control can be direct or indirect. 87 | 88 | **your licenses** are all the licenses granted to you for the software under 89 | these terms. 90 | 91 | **use** means anything you do with the software requiring one of your licenses. 92 | 93 | **trademark** means trademarks, service marks, and similar rights. 94 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Repository Guidelines 2 | 3 | This repository contains a template for building embedded applications using 4 | the Bespoke Simulation framework. For complete template documentation, see 5 | [BESPOKE-TEMPLATE.md](./BESPOKE-TEMPLATE.md). 6 | 7 | ## Overview 8 | 9 | This template provides: 10 | - CodeSignal Design System integration 11 | - Consistent layout components (header, sidebar, main content area) 12 | - Help modal system 13 | - Local development server with WebSocket support 14 | - Standardized file structure and naming conventions 15 | 16 | ## Quick Start 17 | 18 | 1. **Customize the HTML template** (`client/index.html`): 19 | - Replace `` with your page title 20 | - Replace `` with your app name 21 | - Add your main content at `` 22 | - Add app-specific CSS links at `` 23 | - Add app-specific JavaScript at `` 24 | 25 | 2. **Create your application files**: 26 | - App-specific CSS (e.g., `my-app.css`) 27 | - App-specific JavaScript (e.g., `my-app.js`) 28 | - Help content (based on `help-content-template.html`) 29 | 30 | 3. **Start the development server**: 31 | ```bash 32 | npm start 33 | ``` 34 | Server runs on `http://localhost:3000` 35 | 36 | ## Key Conventions 37 | 38 | ### Status Messages 39 | 40 | Use these exact status messages for consistency: 41 | 42 | - "Ready" - Application loaded successfully 43 | - "Loading..." - Data is being loaded 44 | - "Saving..." - Data is being saved 45 | - "Changes saved" - Auto-save completed successfully 46 | - "Save failed (will retry)" - Server save failed, will retry 47 | - "Failed to load data" - Data loading failed 48 | - "Auto-save initialized" - Auto-save system started 49 | 50 | ### File Naming 51 | 52 | - CSS files: kebab-case (e.g., `my-app.css`) 53 | - JavaScript files: kebab-case (e.g., `my-app.js`) 54 | - Data files: kebab-case (e.g., `solution.json`) 55 | - Image files: kebab-case (e.g., `overview.png`) 56 | 57 | ### Error Handling 58 | 59 | - Wrap all async operations in try-catch blocks 60 | - Provide meaningful error messages to users 61 | - Log errors to console for debugging 62 | - Implement retry logic for network operations 63 | - Handle localStorage quota exceeded errors 64 | - Validate data before saving operations 65 | 66 | ## Development Workflow 67 | 68 | ### Build and Test 69 | 70 | ```bash 71 | # Start development server 72 | npm start 73 | 74 | # Development mode (same as start) 75 | npm run dev 76 | ``` 77 | 78 | ### WebSocket Messaging 79 | 80 | The server provides a `POST /message` endpoint for real-time messaging: 81 | 82 | ```bash 83 | curl -X POST http://localhost:3000/message \ 84 | -H "Content-Type: application/json" \ 85 | -d '{"message": "Your message here"}' 86 | ``` 87 | 88 | This sends alerts to connected clients. Requires `ws` package: 89 | ```bash 90 | npm install 91 | ``` 92 | 93 | ## Template Documentation 94 | 95 | For detailed information about: 96 | - Design System usage and components 97 | - CSS implementation guidelines 98 | - JavaScript API (HelpModal, status management) 99 | - Component reference and examples 100 | - Customization options 101 | 102 | See [BESPOKE-TEMPLATE.md](./BESPOKE-TEMPLATE.md). 103 | 104 | ## Project Structure 105 | 106 | ``` 107 | client/ 108 | ├── index.html # Main HTML template 109 | ├── app.js # Application logic 110 | ├── bespoke-template.css # Template-specific styles 111 | ├── help-modal.js # Help modal system 112 | ├── help-content-template.html # Help content template 113 | └── design-system/ # CodeSignal Design System 114 | ├── colors/ 115 | ├── spacing/ 116 | ├── typography/ 117 | └── components/ 118 | server.js # Development server 119 | ``` 120 | 121 | ## Notes for AI Agents 122 | 123 | When working on applications built with this template: 124 | 125 | 1. **Always reference BESPOKE-TEMPLATE.md** for template-specific 126 | implementation details 127 | 2. **Follow the conventions** listed above for status messages and file naming 128 | 3. **Use Design System components** directly - see BESPOKE-TEMPLATE.md for 129 | component classes and usage 130 | 4. **Maintain consistency** with the template's structure and patterns 131 | 5. **Keep guidelines up to date** by editing this AGENTS.md file as the codebase evolves -------------------------------------------------------------------------------- /client/example-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Design System Component Showcase 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |

Component Showcase

32 |
33 |
Ready
34 | 35 |
36 | 37 | 38 |
39 | 40 | 64 | 65 | 66 |
67 |
68 | 69 |
70 |

Counter

71 |
0
72 |
73 | 74 | 75 |
76 |
Active
77 | 78 | 79 |
80 | 81 | 82 |
83 |

Current Settings

84 |
85 | Label: Counter 86 |
87 |
88 | Increment: 1 89 |
90 |
91 |
92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /client/help-content-template.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 19 | 20 | 21 |
22 |

Overview

23 |

This page explains how to use the : .

24 | 25 | 26 |
27 | 28 | 29 |
30 |

Getting Started

31 |

To begin using the :

32 |
    33 |
  1. First step...
  2. 34 |
  3. Second step...
  4. 35 |
  5. Third step...
  6. 36 |
37 | 38 | 39 |
40 | 41 | 42 |
43 |

Key Features

44 | 45 |

Feature 1

46 |

Description of feature 1 and how to use it.

47 | 48 |

Feature 2

49 |

Description of feature 2 and how to use it.

50 | 51 |

Feature 3

52 |

Description of feature 3 and how to use it.

53 | 54 | 55 | 56 |
57 | 58 | 59 |
60 |

Workflow

61 |

Here's the typical workflow for using this application:

62 |
    63 |
  1. Step 1: Description of first workflow step
  2. 64 |
  3. Step 2: Description of second workflow step
  4. 65 |
  5. Step 3: Description of third workflow step
  6. 66 |
  7. Step 4: Description of fourth workflow step
  8. 67 |
68 | 69 |

Tips & Best Practices

70 | 75 |
76 | 77 | 78 |
79 |

Shortcuts

80 | 87 |
88 | 89 | 90 |
91 |

Troubleshooting / FAQ

92 | 93 |
94 | Common question 1? 95 |

Answer to common question 1 with helpful details.

96 |
97 | 98 |
99 | Common question 2? 100 |

Answer to common question 2 with helpful details.

101 |
102 | 103 |
104 | Common question 3? 105 |

Answer to common question 3 with helpful details.

106 |
107 | 108 |
109 | How do I add images to the help? 110 |

Place image files in the help/img/ directory and reference them with relative paths like <img src="./img/example.png" alt="Description">

111 |
112 | 113 |
114 | Is my work automatically saved? 115 |

Yes, the app automatically saves your work. You'll see status messages indicating when saves occur.

116 |
117 |
118 | 119 | 137 | -------------------------------------------------------------------------------- /client/example-app/help-content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 |
16 |

Overview

17 |

This is an interactive showcase application that demonstrates Design System components through a clicker counter app. The app features input controls in the sidebar and visual components in the main content area that update in real-time based on your interactions.

18 |

This showcase demonstrates:

19 | 25 |
26 | 27 | 28 |
29 |

Sidebar Controls

30 |

The sidebar contains input controls that affect the counter:

31 | 32 |

Counter Label

33 |

Use the text input field to change the label displayed above the counter value. Type any text you want, and it will update immediately in the main display area.

34 | 35 |

Increment Amount

36 |

Select the increment amount from the dropdown menu. You can choose:

37 | 43 |

The selected increment amount is displayed in the settings panel below the counter.

44 | 45 |

Action Buttons

46 | 51 |
52 | 53 | 54 |
55 |

Main Display Area

56 |

The main content area shows visual components that update based on your interactions:

57 | 58 |

Counter Display

59 |

The counter value is displayed prominently in a card box component. The label above the counter updates when you change it in the sidebar.

60 | 61 |

Status Tags

62 |

Status tags automatically update based on the counter value:

63 | 68 | 69 |

Action Buttons

70 |

Action buttons in the display area mirror the sidebar controls. You can use either set of buttons to control the counter.

71 | 72 |

Settings Display

73 |

The settings panel shows the current label and increment amount in a read-only card component, providing a clear view of the current configuration.

74 |
75 | 76 | 77 |
78 |

Design System Components Used

79 |

This app demonstrates the following Design System components:

80 | 81 |

Buttons

82 | 87 | 88 |

Boxes/Cards

89 | 92 | 93 |

Tags

94 | 99 | 100 |

Dropdown

101 | 104 | 105 |

Input

106 | 109 |
110 | -------------------------------------------------------------------------------- /client/help-modal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HelpModal - A reusable, dependency-free help modal system for Bespoke applications 3 | * 4 | * This modal system is designed to work with the CodeSignal Design System and provides 5 | * a consistent help experience across all embedded applications. 6 | * 7 | * Note: Currently uses temporary modal components from bespoke-template.css. 8 | * When modal components are added to the design system, this should be updated to use them. 9 | * 10 | * Usage: 11 | * HelpModal.init({ 12 | * triggerSelector: '#btn-help', 13 | * content: helpContent, 14 | * theme: 'auto' 15 | * }); 16 | */ 17 | 18 | class HelpModal { 19 | constructor(options = {}) { 20 | this.options = { 21 | triggerSelector: '#btn-help', 22 | content: '', 23 | theme: 'auto', // 'light', 'dark', or 'auto' 24 | customStyles: {}, 25 | ...options 26 | }; 27 | 28 | this.isOpen = false; 29 | this.modal = null; 30 | this.trigger = null; 31 | 32 | this.init(); 33 | } 34 | 35 | init() { 36 | this.createModal(); 37 | this.bindEvents(); 38 | } 39 | 40 | createModal() { 41 | // Create modal container using temporary modal classes from bespoke-template.css 42 | // TODO: Update to use design system modal classes when available 43 | this.modal = document.createElement('div'); 44 | this.modal.className = 'modal'; 45 | this.modal.innerHTML = ` 46 | 47 | 56 | `; 57 | 58 | // Initially hidden 59 | this.modal.style.display = 'none'; 60 | document.body.appendChild(this.modal); 61 | } 62 | 63 | bindEvents() { 64 | // Find trigger element 65 | this.trigger = document.querySelector(this.options.triggerSelector); 66 | if (!this.trigger) { 67 | console.warn(`HelpModal: Trigger element '${this.options.triggerSelector}' not found`); 68 | return; 69 | } 70 | 71 | // Convert link to button if needed 72 | if (this.trigger.tagName === 'A') { 73 | this.trigger.addEventListener('click', (e) => { 74 | e.preventDefault(); 75 | this.open(); 76 | }); 77 | } else { 78 | this.trigger.addEventListener('click', () => this.open()); 79 | } 80 | 81 | // Close button 82 | const closeBtn = this.modal.querySelector('.modal-close'); 83 | closeBtn.addEventListener('click', () => this.close()); 84 | 85 | // Backdrop click 86 | const backdrop = this.modal.querySelector('.modal-backdrop'); 87 | backdrop.addEventListener('click', () => this.close()); 88 | 89 | // ESC key 90 | document.addEventListener('keydown', (e) => { 91 | if (e.key === 'Escape' && this.isOpen) { 92 | this.close(); 93 | } 94 | }); 95 | 96 | // Handle internal navigation links 97 | this.modal.addEventListener('click', (e) => { 98 | if (e.target.matches('a[href^="#"]')) { 99 | e.preventDefault(); 100 | const targetId = e.target.getAttribute('href').substring(1); 101 | const targetElement = this.modal.querySelector(`#${targetId}`); 102 | if (targetElement) { 103 | targetElement.scrollIntoView({ behavior: 'smooth' }); 104 | } 105 | } 106 | }); 107 | } 108 | 109 | open() { 110 | if (this.isOpen) return; 111 | 112 | this.isOpen = true; 113 | this.modal.style.display = 'flex'; // Use flex to center the modal 114 | document.body.style.overflow = 'hidden'; // Prevent background scrolling 115 | 116 | // Focus management 117 | const closeBtn = this.modal.querySelector('.modal-close'); 118 | closeBtn.focus(); 119 | 120 | // Trigger custom event 121 | this.trigger.dispatchEvent(new CustomEvent('helpModal:open', { detail: this })); 122 | } 123 | 124 | close() { 125 | if (!this.isOpen) return; 126 | 127 | this.isOpen = false; 128 | this.modal.style.display = 'none'; 129 | document.body.style.overflow = ''; // Restore scrolling 130 | 131 | // Return focus to trigger 132 | this.trigger.focus(); 133 | 134 | // Trigger custom event 135 | this.trigger.dispatchEvent(new CustomEvent('helpModal:close', { detail: this })); 136 | } 137 | 138 | // Public API methods 139 | static init(options) { 140 | return new HelpModal(options); 141 | } 142 | 143 | destroy() { 144 | if (this.modal && this.modal.parentNode) { 145 | this.modal.parentNode.removeChild(this.modal); 146 | } 147 | document.body.style.overflow = ''; 148 | } 149 | 150 | // Method to update content dynamically 151 | updateContent(newContent) { 152 | const modalBody = this.modal.querySelector('.modal-body'); 153 | if (modalBody) { 154 | modalBody.innerHTML = newContent; 155 | } 156 | } 157 | } 158 | 159 | // Export for use 160 | if (typeof module !== 'undefined' && module.exports) { 161 | module.exports = HelpModal; 162 | } else { 163 | window.HelpModal = HelpModal; 164 | } 165 | -------------------------------------------------------------------------------- /client/example-app/example-app.js: -------------------------------------------------------------------------------- 1 | // example-app.js 2 | // Interactive Component Showcase Application 3 | 4 | (function() { 5 | const status = document.getElementById('status'); 6 | 7 | // App state 8 | let counterValue = 0; 9 | let incrementAmount = 1; 10 | let counterLabel = 'Counter'; 11 | let dropdownInstance = null; 12 | 13 | function setStatus(msg) { 14 | if (status) { 15 | status.textContent = msg; 16 | } 17 | } 18 | 19 | // Update counter display 20 | function updateCounterDisplay() { 21 | const counterDisplay = document.getElementById('counter-value'); 22 | const labelDisplay = document.getElementById('display-label'); 23 | const labelValueDisplay = document.getElementById('display-label-value'); 24 | const incrementValueDisplay = document.getElementById('display-increment-value'); 25 | 26 | if (counterDisplay) { 27 | counterDisplay.textContent = counterValue; 28 | } 29 | 30 | if (labelDisplay) { 31 | labelDisplay.textContent = counterLabel; 32 | } 33 | 34 | if (labelValueDisplay) { 35 | labelValueDisplay.textContent = counterLabel; 36 | } 37 | 38 | if (incrementValueDisplay) { 39 | incrementValueDisplay.textContent = incrementAmount; 40 | } 41 | 42 | // Update status tags 43 | updateStatusTags(); 44 | } 45 | 46 | // Update status tags based on counter value 47 | function updateStatusTags() { 48 | const primaryTag = document.getElementById('status-tag-primary'); 49 | const positiveTag = document.getElementById('status-tag-positive'); 50 | const negativeTag = document.getElementById('status-tag-negative'); 51 | 52 | if (counterValue > 0) { 53 | if (primaryTag) primaryTag.style.display = 'none'; 54 | if (positiveTag) positiveTag.style.display = 'inline-block'; 55 | if (negativeTag) negativeTag.style.display = 'none'; 56 | } else if (counterValue < 0) { 57 | if (primaryTag) primaryTag.style.display = 'none'; 58 | if (positiveTag) positiveTag.style.display = 'none'; 59 | if (negativeTag) negativeTag.style.display = 'inline-block'; 60 | } else { 61 | if (primaryTag) primaryTag.style.display = 'inline-block'; 62 | if (positiveTag) positiveTag.style.display = 'none'; 63 | if (negativeTag) negativeTag.style.display = 'none'; 64 | } 65 | } 66 | 67 | // Increment counter 68 | function incrementCounter() { 69 | counterValue += incrementAmount; 70 | updateCounterDisplay(); 71 | setStatus('Counter incremented'); 72 | } 73 | 74 | // Decrement counter 75 | function decrementCounter() { 76 | counterValue -= incrementAmount; 77 | updateCounterDisplay(); 78 | setStatus('Counter decremented'); 79 | } 80 | 81 | // Reset counter 82 | function resetCounter() { 83 | counterValue = 0; 84 | updateCounterDisplay(); 85 | setStatus('Counter reset'); 86 | } 87 | 88 | // Initialize dropdown component 89 | function initializeDropdown() { 90 | if (typeof window.Dropdown === 'undefined') { 91 | console.error('Dropdown class not found. Make sure dropdown.js is loaded.'); 92 | return; 93 | } 94 | 95 | const dropdownItems = [ 96 | { value: '1', label: '1' }, 97 | { value: '5', label: '5' }, 98 | { value: '10', label: '10' }, 99 | { value: '25', label: '25' } 100 | ]; 101 | 102 | try { 103 | dropdownInstance = new window.Dropdown('#increment-dropdown', { 104 | items: dropdownItems, 105 | selectedValue: '1', 106 | placeholder: 'Select increment amount', 107 | onSelect: (value) => { 108 | incrementAmount = parseInt(value, 10); 109 | updateCounterDisplay(); 110 | setStatus(`Increment amount set to ${incrementAmount}`); 111 | } 112 | }); 113 | } catch (error) { 114 | console.error('Error initializing dropdown:', error); 115 | } 116 | } 117 | 118 | // Initialize event listeners 119 | function initializeEventListeners() { 120 | // Sidebar controls 121 | const btnIncrement = document.getElementById('btn-increment'); 122 | const btnDecrement = document.getElementById('btn-decrement'); 123 | const btnReset = document.getElementById('btn-reset'); 124 | const counterLabelInput = document.getElementById('counter-label'); 125 | 126 | // Increment button 127 | if (btnIncrement) { 128 | btnIncrement.addEventListener('click', incrementCounter); 129 | } 130 | 131 | // Decrement button 132 | if (btnDecrement) { 133 | btnDecrement.addEventListener('click', decrementCounter); 134 | } 135 | 136 | // Reset button 137 | if (btnReset) { 138 | btnReset.addEventListener('click', resetCounter); 139 | } 140 | 141 | // Label input 142 | if (counterLabelInput) { 143 | counterLabelInput.addEventListener('input', (e) => { 144 | counterLabel = e.target.value || 'Counter'; 145 | updateCounterDisplay(); 146 | setStatus('Label updated'); 147 | }); 148 | } 149 | } 150 | 151 | // Initialize help modal 152 | async function initializeHelpModal() { 153 | try { 154 | const response = await fetch('./help-content.html'); 155 | const helpContent = await response.text(); 156 | 157 | if (typeof HelpModal !== 'undefined') { 158 | HelpModal.init({ 159 | triggerSelector: '#btn-help', 160 | content: helpContent, 161 | theme: 'auto' 162 | }); 163 | } else { 164 | console.error('HelpModal not found. Make sure help-modal.js is loaded.'); 165 | } 166 | } catch (error) { 167 | console.error('Failed to load help content:', error); 168 | if (typeof HelpModal !== 'undefined') { 169 | HelpModal.init({ 170 | triggerSelector: '#btn-help', 171 | content: '

Help content could not be loaded. Please check that help-content.html exists.

', 172 | theme: 'auto' 173 | }); 174 | } 175 | } 176 | } 177 | 178 | // Initialize everything when DOM is ready 179 | function initialize() { 180 | setStatus('Loading...'); 181 | 182 | // Initialize event listeners 183 | initializeEventListeners(); 184 | 185 | // Initialize help modal 186 | initializeHelpModal(); 187 | 188 | // Initialize dropdown after a short delay to ensure Dropdown class is loaded 189 | setTimeout(() => { 190 | initializeDropdown(); 191 | updateCounterDisplay(); 192 | setStatus('Ready'); 193 | }, 100); 194 | } 195 | 196 | if (document.readyState === 'loading') { 197 | document.addEventListener('DOMContentLoaded', initialize); 198 | } else { 199 | initialize(); 200 | } 201 | })(); 202 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const url = require('url'); 5 | 6 | // Try to load WebSocket module, fallback if not available 7 | let WebSocket = null; 8 | let isWebSocketAvailable = false; 9 | try { 10 | WebSocket = require('ws'); 11 | isWebSocketAvailable = true; 12 | console.log('WebSocket support enabled'); 13 | } catch (error) { 14 | console.log('WebSocket support disabled (ws package not installed)'); 15 | console.log('Install with: npm install ws'); 16 | } 17 | 18 | const DIST_DIR = path.join(__dirname, 'dist'); 19 | // Check if IS_PRODUCTION is set to true 20 | const isProduction = process.env.IS_PRODUCTION === 'true'; 21 | // In production mode, dist directory must exist 22 | if (isProduction && !fs.existsSync(DIST_DIR)) { 23 | throw new Error(`Production mode enabled but dist directory does not exist: ${DIST_DIR}`); 24 | } 25 | // Force port 3000 in production, otherwise use PORT environment variable or default to 3000 26 | const PORT = isProduction ? 3000 : (process.env.PORT || 3000); 27 | 28 | // Track connected WebSocket clients 29 | const wsClients = new Set(); 30 | 31 | // MIME types for different file extensions 32 | const mimeTypes = { 33 | '.html': 'text/html', 34 | '.js': 'text/javascript', 35 | '.css': 'text/css', 36 | '.json': 'application/json', 37 | '.png': 'image/png', 38 | '.jpg': 'image/jpeg', 39 | '.jpeg': 'image/jpeg', 40 | '.gif': 'image/gif', 41 | '.svg': 'image/svg+xml', 42 | '.ico': 'image/x-icon', 43 | '.woff': 'font/woff', 44 | '.woff2': 'font/woff2', 45 | '.ttf': 'font/ttf', 46 | '.eot': 'application/vnd.ms-fontobject' 47 | }; 48 | 49 | // Get MIME type based on file extension 50 | function getMimeType(filePath) { 51 | const ext = path.extname(filePath).toLowerCase(); 52 | return mimeTypes[ext] || 'text/plain'; 53 | } 54 | 55 | // Serve static files 56 | function serveFile(filePath, res) { 57 | fs.readFile(filePath, (err, data) => { 58 | if (err) { 59 | res.writeHead(404, { 'Content-Type': 'text/plain' }); 60 | res.end('File not found'); 61 | return; 62 | } 63 | 64 | const mimeType = getMimeType(filePath); 65 | res.writeHead(200, { 'Content-Type': mimeType }); 66 | res.end(data); 67 | }); 68 | } 69 | 70 | // Handle POST requests 71 | function handlePostRequest(req, res, parsedUrl) { 72 | if (parsedUrl.pathname === '/message') { 73 | let body = ''; 74 | 75 | req.on('data', chunk => { 76 | body += chunk.toString(); 77 | }); 78 | 79 | req.on('end', () => { 80 | try { 81 | const data = JSON.parse(body); 82 | const message = data.message; 83 | 84 | if (!message) { 85 | res.writeHead(400, { 'Content-Type': 'application/json' }); 86 | res.end(JSON.stringify({ error: 'Message is required' })); 87 | return; 88 | } 89 | 90 | // Check if WebSocket is available 91 | if (!isWebSocketAvailable) { 92 | res.writeHead(503, { 'Content-Type': 'application/json' }); 93 | res.end(JSON.stringify({ 94 | error: 'WebSocket functionality not available', 95 | details: 'Install the ws package with: npm install ws' 96 | })); 97 | return; 98 | } 99 | 100 | // Broadcast message to all connected WebSocket clients 101 | wsClients.forEach(client => { 102 | if (client.readyState === WebSocket.OPEN) { 103 | client.send(JSON.stringify({ type: 'message', message: message })); 104 | } 105 | }); 106 | 107 | res.writeHead(200, { 'Content-Type': 'application/json' }); 108 | res.end(JSON.stringify({ success: true, clientCount: wsClients.size })); 109 | 110 | } catch (error) { 111 | res.writeHead(400, { 'Content-Type': 'application/json' }); 112 | res.end(JSON.stringify({ error: 'Invalid JSON' })); 113 | } 114 | }); 115 | } else { 116 | res.writeHead(404, { 'Content-Type': 'text/plain' }); 117 | res.end('Not found'); 118 | } 119 | } 120 | 121 | // Create HTTP server 122 | const server = http.createServer((req, res) => { 123 | const parsedUrl = url.parse(req.url, true); 124 | let pathName = parsedUrl.pathname === '/' ? '/index.html' : parsedUrl.pathname; 125 | 126 | // Handle POST requests 127 | if (req.method === 'POST') { 128 | handlePostRequest(req, res, parsedUrl); 129 | return; 130 | } 131 | 132 | // In production mode, serve static files from dist directory 133 | if (isProduction) { 134 | // Strip leading slashes so path.join/resolve can't ignore DIST_DIR 135 | let filePath = path.join(DIST_DIR, pathName.replace(/^\/+/, '')); 136 | 137 | // Security check - prevent directory traversal 138 | const resolvedDistDir = path.resolve(DIST_DIR); 139 | const resolvedFilePath = path.resolve(filePath); 140 | const relativePath = path.relative(resolvedDistDir, resolvedFilePath); 141 | 142 | // Reject if path tries to traverse outside the base directory 143 | if (relativePath.startsWith('..')) { 144 | res.writeHead(403, { 'Content-Type': 'text/plain' }); 145 | res.end('Forbidden'); 146 | return; 147 | } 148 | 149 | serveFile(filePath, res); 150 | } else { 151 | // Development mode - static files are served by Vite 152 | res.writeHead(404, { 'Content-Type': 'text/plain' }); 153 | res.end('Not found (development mode - use Vite dev server `npm run start:dev`)'); 154 | } 155 | }); 156 | 157 | // Create WebSocket server only if WebSocket is available 158 | // Note: WebSocket upgrade handling is performed automatically by the ws library 159 | // when attached to the HTTP server. The HTTP request handler should NOT send 160 | // a response for upgrade requests - the ws library handles the upgrade internally. 161 | if (isWebSocketAvailable) { 162 | const wss = new WebSocket.Server({ 163 | server, 164 | path: '/ws' 165 | }); 166 | 167 | wss.on('connection', (ws, req) => { 168 | console.log('New WebSocket client connected'); 169 | wsClients.add(ws); 170 | 171 | ws.on('close', () => { 172 | console.log('WebSocket client disconnected'); 173 | wsClients.delete(ws); 174 | }); 175 | 176 | ws.on('error', (error) => { 177 | console.error('WebSocket error:', error); 178 | wsClients.delete(ws); 179 | }); 180 | }); 181 | } 182 | 183 | // Start server 184 | server.listen(PORT, () => { 185 | console.log(`Server running at http://localhost:${PORT}`); 186 | if (isProduction) { 187 | console.log(`Serving static files from: ${DIST_DIR}`); 188 | } else { 189 | console.log(`Development mode - static files served by Vite`); 190 | } 191 | if (isWebSocketAvailable) { 192 | console.log(`WebSocket server running on /ws`); 193 | } else { 194 | console.log(`WebSocket functionality disabled - install 'ws' package to enable`); 195 | } 196 | console.log('Press Ctrl+C to stop the server'); 197 | }); 198 | 199 | // Handle server errors 200 | server.on('error', (err) => { 201 | if (err.code === 'EADDRINUSE') { 202 | console.error(`Port ${PORT} is already in use. Please try a different port.`); 203 | } else { 204 | console.error('Server error:', err); 205 | } 206 | process.exit(1); 207 | }); 208 | 209 | // Graceful shutdown 210 | process.on('SIGINT', () => { 211 | console.log('\nShutting down server...'); 212 | server.close(() => { 213 | console.log('Server closed'); 214 | process.exit(0); 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bespoke Simulation Template 2 | 3 | This directory contains a template for creating embedded applications that share a consistent design system and user experience. 4 | 5 | ## Components 6 | 7 | ### 1. Design System Integration 8 | This template uses the CodeSignal Design System located in `client/design-system/`: 9 | - **Foundations**: Colors, spacing, typography tokens 10 | - **Components**: Buttons, boxes, inputs, dropdowns, tags 11 | - Light and dark theme support (automatic) 12 | - See the [design system repository](https://github.com/CodeSignal/learn_bespoke-design-system) for full documentation 13 | 14 | ### 2. `client/bespoke-template.css` 15 | Template-specific CSS providing: 16 | - Layout components (header, sidebar, main-layout) 17 | - Utility classes (row, spacer, status) 18 | - Temporary components (modals, form elements) - will be replaced when design system adds them 19 | 20 | ### 3. `client/index.html` 21 | A base HTML template that includes: 22 | - Navigation header with app name and help button 23 | - Main layout structure (sidebar + content area) 24 | - Help modal integration 25 | - Proper CSS and JavaScript loading 26 | 27 | ### 4. `client/help-modal.js` 28 | A dependency-free JavaScript module for the help modal system: 29 | - Consistent modal behavior across all apps 30 | - Keyboard navigation (ESC to close) 31 | - Focus management 32 | - Custom event system 33 | 34 | ### 5. `client/help-content-template.html` 35 | A template for creating consistent help content: 36 | - Table of contents navigation 37 | - Standardized section structure 38 | - FAQ with collapsible details 39 | - Image integration guidelines 40 | 41 | ## Usage Instructions 42 | 43 | ### Setting Up a New Application 44 | 45 | 1. **Clone the repository** 46 | 2. **Ensure the design-system submodule is initialized**: 47 | ```bash 48 | git submodule update --init --recursive 49 | ``` 50 | 51 | 3. **Customize the HTML template** by replacing placeholders: 52 | - `` - Your application title 53 | - `` - Your application name (appears in header) 54 | - `` - Any additional header elements 55 | - `` - Your main content area 56 | - `` - Links to your app-specific CSS files 57 | - `` - Links to your app-specific JavaScript files 58 | 59 | 3. **Use Design System Components** 60 | The template uses design system components directly. Use these classes: 61 | - Buttons: `button button-primary`, `button button-secondary`, `button button-danger`, `button button-text` 62 | - Boxes/Cards: `box card` for card containers 63 | - Inputs: Add `input` class to input elements: `` 64 | 65 | 4. **Implement your application logic**. You can use Cursor or other agents for it. There is a file called `AGENTS.md` that contains context LLM can use. 66 | 5. **Customise your help content** using the help content template 67 | 3. **Use Design System Components** 68 | The template uses design system components directly. Use these classes: 69 | - Buttons: `button button-primary`, `button button-secondary`, `button button-danger`, `button button-text` 70 | - Boxes/Cards: `box card` for card containers 71 | - Inputs: Add `input` class to input elements: `` 72 | 73 | 4. **Implement your application logic**. You can use Cursor or other agents for it. There is a file called `AGENTS.md` that contains context LLM can use. 74 | 5. **Customise your help content** using the help content template 75 | 76 | ### Customizing Help Content 77 | 78 | Use the `help-content-template.html` as a starting point: 79 | 80 | 1. **Replace placeholders** like `` with your actual content 81 | 2. **Add sections** as needed for your application 82 | 3. **Include images** by placing them in a `help/img/` directory 83 | 4. **Use the provided structure** for consistency across applications 84 | 85 | 86 | ### Help Modal API 87 | 88 | The `HelpModal` class provides several methods: 89 | 90 | ```javascript 91 | // Initialize 92 | const modal = HelpModal.init({ 93 | triggerSelector: '#btn-help', 94 | content: helpContent, 95 | theme: 'auto' 96 | }); 97 | 98 | // Update content dynamically 99 | modal.updateContent(newHelpContent); 100 | 101 | // Destroy the modal 102 | modal.destroy(); 103 | ``` 104 | 105 | ## Server 106 | 107 | This template includes a local development server (`server.js`) that provides: 108 | - Static file serving for your application 109 | - WebSocket support for real-time messaging 110 | - A REST API for triggering client-side alerts 111 | 112 | ### Starting the Server 113 | 114 | ```bash 115 | # Local development 116 | npm run start:dev # Vite + API for local development 117 | # Production 118 | npm run build # Create production build in dist/ 119 | npm run start:prod # Serve built assets from dist/ 120 | ``` 121 | 122 | 123 | ### Environment Variables 124 | 125 | The server supports the following environment variables: 126 | 127 | - **`PORT`** - Server port number 128 | - Development: Can be set to any port (e.g., `PORT=3001`), defaulting to `3000` 129 | - Production: Ignored (always `3000` when `IS_PRODUCTION=true`) 130 | 131 | - **`IS_PRODUCTION`** - Enables production mode 132 | - Set to `'true'` to enable production mode 133 | - When enabled: 134 | - Server serves static files from `dist/` directory 135 | - Port is forced to `3000` 136 | - Requires `dist/` directory to exist (throws error if missing) 137 | 138 | 139 | ### Vite Build System 140 | 141 | This project uses [Vite](https://vitejs.dev/) as the build tool for fast development and optimized production builds. 142 | 143 | #### Build Process 144 | 145 | Running `npm run build` executes `vite build`, which: 146 | - Reads source files from the `client/` directory (configured in `vite.config.js`) 147 | - Processes and bundles JavaScript, CSS, and other assets 148 | - Outputs optimized production files to the `dist/` directory 149 | - Generates hashed filenames for cache busting 150 | 151 | ### WebSocket Messaging API 152 | 153 | The server provides a `POST /message` endpoint that allows you to send real-time messages to connected clients. This can be used to signal changes in the client during events like "Run" or "Submit". When a message is sent, the preview window with the application open will display an alert with the message. 154 | 155 | It uses the `ws` package, so if you want to use it, install the packages (but this is optional). 156 | 157 | ``` 158 | npm install 159 | ``` 160 | 161 | #### Endpoint: `POST /message` 162 | 163 | **Request Format:** 164 | ```json 165 | { 166 | "message": "Your message here" 167 | } 168 | ``` 169 | 170 | **Example using curl:** 171 | ```bash 172 | curl -X POST http://localhost:3000/message \ 173 | -H "Content-Type: application/json" \ 174 | -d '{"message": "Hello from the server!"}' 175 | ``` 176 | 177 | ## CI/CD and Automated Releases 178 | 179 | This template includes a GitHub Actions workflow (`.github/workflows/build-release.yml`) that automatically builds and releases your application when you push to the `main` branch. 180 | 181 | ### How It Works 182 | 183 | When you push to `main`, the workflow will: 184 | 185 | 1. **Build the project** - Runs `npm run build` to create production assets in `dist/` 186 | 2. **Create a release tarball** - Packages `dist/`, `package.json`, `server.js`, and production `node_modules/` into `release.tar.gz` 187 | 3. **Create a GitHub Release** - Automatically creates a new release tagged as `v{run_number}` with the tarball attached 188 | 189 | ### Release Contents 190 | 191 | The release tarball (`release.tar.gz`) contains everything needed to deploy the application: 192 | - `dist/` - Built production assets 193 | - `package.json` - Project dependencies and scripts 194 | - `server.js` - Production server 195 | - `node_modules/` - Production dependencies only 196 | 197 | ### Using Releases 198 | 199 | To deploy a release: 200 | 201 | 1. Download `release.tar.gz` from the latest GitHub Release (e.g. with `wget`) 202 | 2. Extract (and remove) the tarball: `tar -xzf release.tar.gz && rm release.tar.gz` 203 | 3. Start the production server: `npm run start:prod` 204 | -------------------------------------------------------------------------------- /client/bespoke-template.css: -------------------------------------------------------------------------------- 1 | /* ===== BESPOKE TEMPLATE CSS ===== */ 2 | /* Template-specific components using CodeSignal Design System tokens */ 3 | /* This file provides layout, utilities, and temporary components not yet in the design system */ 4 | 5 | /* ===== LAYOUT COMPONENTS ===== */ 6 | 7 | /* Bespoke wrapper for scoping */ 8 | .bespoke { 9 | font-family: var(--body-family); 10 | color: var(--Colors-Text-Body-Default); 11 | background: var(--Colors-Backgrounds-Main-Default); 12 | line-height: 1.6; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | .bespoke * { 18 | box-sizing: border-box; 19 | } 20 | 21 | /* Header */ 22 | .bespoke .header { 23 | display: flex; 24 | align-items: center; 25 | gap: var(--UI-Spacing-spacing-ml); 26 | padding: var(--UI-Spacing-spacing-s) var(--UI-Spacing-spacing-mxl); 27 | border-bottom: 1px solid var(--Colors-Stroke-Default); 28 | background: var(--Colors-Backgrounds-Main-Top); 29 | width: 100%; 30 | } 31 | 32 | .bespoke .header h1 { 33 | font-size: var(--Fonts-Body-Default-lg); 34 | margin: 0; 35 | font-weight: 600; 36 | font-family: var(--heading-family); 37 | color: var(--Colors-Text-Body-Strongest); 38 | } 39 | 40 | .bespoke .header .status { 41 | font-size: var(--Fonts-Body-Default-xs); 42 | color: var(--Colors-Text-Body-Medium); 43 | } 44 | 45 | /* Main Layout */ 46 | .bespoke .main-layout { 47 | display: grid; 48 | grid-template-columns: 300px 1fr; 49 | height: calc(100% - 42px); 50 | } 51 | 52 | .bespoke .sidebar { 53 | padding: var(--UI-Spacing-spacing-s); 54 | overflow: auto; 55 | border-right: 1px solid var(--Colors-Stroke-Default); 56 | background: var(--Colors-Backgrounds-Main-Default); 57 | } 58 | 59 | .bespoke .content-area { 60 | width: 100%; 61 | height: 100%; 62 | } 63 | 64 | /* ===== UTILITY CLASSES ===== */ 65 | 66 | /* Flexbox Utilities */ 67 | .bespoke .row { 68 | display: flex; 69 | align-items: center; 70 | gap: var(--UI-Spacing-spacing-s); 71 | } 72 | 73 | .bespoke .row-between { 74 | display: flex; 75 | align-items: center; 76 | justify-content: space-between; 77 | gap: var(--UI-Spacing-spacing-ml); 78 | } 79 | 80 | .bespoke .spacer { 81 | flex: 1; 82 | } 83 | 84 | /* Dividers */ 85 | .bespoke hr { 86 | border: none; 87 | border-top: 0.5px solid var(--Colors-Stroke-Default); 88 | margin: var(--UI-Spacing-spacing-ml) 0; 89 | } 90 | 91 | /* ===== TEMPORARY COMPONENTS (TODO: Replace when design system adds these) ===== */ 92 | 93 | /* Modal Components - TODO: Remove when design system adds modal component */ 94 | .bespoke .modal { 95 | position: fixed; 96 | top: 0; 97 | left: 0; 98 | width: 100%; 99 | height: 100%; 100 | z-index: 500; 101 | display: flex; 102 | align-items: center; 103 | justify-content: center; 104 | padding: var(--UI-Spacing-spacing-xl); 105 | box-sizing: border-box; 106 | margin: 0; 107 | } 108 | 109 | .bespoke .modal-backdrop { 110 | position: absolute; 111 | top: 0; 112 | left: 0; 113 | width: 100%; 114 | height: 100%; 115 | background: rgba(0, 0, 0, 0.5); 116 | backdrop-filter: blur(2px); 117 | } 118 | 119 | .bespoke .modal-content { 120 | position: relative; 121 | background: var(--Colors-Backgrounds-Main-Top); 122 | border: 1px solid var(--Colors-Stroke-Default); 123 | border-radius: var(--UI-Radius-radius-m); 124 | max-width: 800px; 125 | width: calc(100% - 40px); 126 | max-height: 90vh; 127 | display: flex; 128 | flex-direction: column; 129 | box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 130 | margin: 0; 131 | } 132 | 133 | .bespoke .modal-header { 134 | display: flex; 135 | align-items: center; 136 | justify-content: space-between; 137 | padding: var(--UI-Spacing-spacing-xl); 138 | border-bottom: 1px solid var(--Colors-Stroke-Default); 139 | background: var(--Colors-Backgrounds-Main-Top); 140 | border-radius: var(--UI-Radius-radius-m) var(--UI-Radius-radius-m) 0 0; 141 | } 142 | 143 | .bespoke .modal-header h2 { 144 | margin: 0; 145 | font-size: var(--Fonts-Body-Default-xl); 146 | color: var(--Colors-Text-Body-Strongest); 147 | font-family: var(--heading-family); 148 | font-weight: 500; 149 | } 150 | 151 | .bespoke .modal-close { 152 | background: none; 153 | border: none; 154 | font-size: var(--Fonts-Body-Default-xxxl); 155 | color: var(--Colors-Text-Body-Medium); 156 | cursor: pointer; 157 | padding: var(--UI-Spacing-spacing-xxs) var(--UI-Spacing-spacing-s); 158 | border-radius: var(--UI-Radius-radius-xxs); 159 | line-height: 1; 160 | transition: all 0.2s ease; 161 | } 162 | 163 | .bespoke .modal-close:hover { 164 | background: var(--Colors-Backgrounds-Main-Medium); 165 | color: var(--Colors-Text-Body-Default); 166 | } 167 | 168 | .bespoke .modal-body { 169 | padding: var(--UI-Spacing-spacing-xl); 170 | overflow-y: auto; 171 | flex: 1; 172 | line-height: 1.6; 173 | } 174 | 175 | .bespoke .modal-body h2 { 176 | margin-top: var(--UI-Spacing-spacing-xxxl); 177 | margin-bottom: var(--UI-Spacing-spacing-ml); 178 | font-size: var(--Fonts-Body-Default-xl); 179 | color: var(--Colors-Text-Body-Strongest); 180 | font-family: var(--heading-family); 181 | font-weight: 500; 182 | } 183 | 184 | .bespoke .modal-body h2:first-child { 185 | margin-top: 0; 186 | } 187 | 188 | .bespoke .modal-body h3 { 189 | margin-top: var(--UI-Spacing-spacing-xl); 190 | margin-bottom: var(--UI-Spacing-spacing-s); 191 | font-size: var(--Fonts-Body-Default-lg); 192 | color: var(--Colors-Text-Body-Strongest); 193 | font-family: var(--heading-family); 194 | font-weight: 500; 195 | } 196 | 197 | .bespoke .modal-body p, 198 | .bespoke .modal-body li { 199 | color: var(--Colors-Text-Body-Default); 200 | margin-bottom: var(--UI-Spacing-spacing-s); 201 | } 202 | 203 | .bespoke .modal-body ul, 204 | .bespoke .modal-body ol { 205 | margin: var(--UI-Spacing-spacing-s) 0 var(--UI-Spacing-spacing-ml) 0; 206 | padding-left: var(--UI-Spacing-spacing-xl); 207 | } 208 | 209 | .bespoke .modal-body code { 210 | font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; 211 | background: rgba(148, 163, 184, 0.12); 212 | border-radius: var(--UI-Radius-radius-xxs); 213 | padding: 0.15em 0.35em; 214 | font-size: var(--Fonts-Body-Default-xs); 215 | } 216 | 217 | .bespoke .modal-body pre { 218 | font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; 219 | background: rgba(148, 163, 184, 0.12); 220 | border-radius: var(--UI-Radius-radius-xs); 221 | padding: var(--UI-Spacing-spacing-ms); 222 | overflow: auto; 223 | margin: var(--UI-Spacing-spacing-ml) 0; 224 | } 225 | 226 | .bespoke .modal-body img, 227 | .bespoke .modal-body video { 228 | max-width: 100%; 229 | height: auto; 230 | border-radius: var(--UI-Radius-radius-xs); 231 | border: 1px solid var(--Colors-Stroke-Default); 232 | margin: var(--UI-Spacing-spacing-ml) 0; 233 | } 234 | 235 | /* Form Elements - TODO: Remove when design system adds form components */ 236 | .bespoke label { 237 | display: flex; 238 | flex-direction: column; 239 | gap: var(--UI-Spacing-spacing-xxs); 240 | margin: var(--UI-Spacing-spacing-ms) 0 var(--UI-Spacing-spacing-s) 0; 241 | } 242 | 243 | .bespoke label.row { 244 | flex-direction: row; 245 | align-items: center; 246 | gap: var(--UI-Spacing-spacing-s); 247 | margin: var(--UI-Spacing-spacing-s) 0; 248 | } 249 | 250 | /* Textarea */ 251 | .bespoke textarea { 252 | padding: var(--UI-Spacing-spacing-ms); 253 | border: 1px solid var(--Colors-Input-Border-Default); 254 | border-radius: var(--UI-Radius-radius-s); 255 | background: var(--Colors-Input-Background-Default); 256 | color: var(--Colors-Input-Text-Default); 257 | font-family: var(--body-family); 258 | font-size: var(--Fonts-Body-Default-md); 259 | min-height: 6rem; 260 | resize: vertical; 261 | transition: border-color 0.2s ease; 262 | } 263 | 264 | .bespoke textarea:hover { 265 | border-color: var(--Colors-Input-Border-Hover); 266 | } 267 | 268 | .bespoke textarea:focus-visible { 269 | outline: none; 270 | border-color: var(--Colors-Input-Border-Focus); 271 | box-shadow: 0 0 0 4px var(--Colors-Input-Shadow-Focus); 272 | } 273 | 274 | .bespoke textarea::placeholder { 275 | color: var(--Colors-Input-Text-Placeholder); 276 | opacity: 1; 277 | } 278 | 279 | /* Select styling - TODO: Remove when design system adds select component */ 280 | .bespoke select { 281 | -webkit-appearance: none; 282 | -moz-appearance: none; 283 | appearance: none; 284 | background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e"); 285 | background-repeat: no-repeat; 286 | background-position: right 0.75rem center; 287 | background-size: 1rem; 288 | padding-right: 3rem; 289 | } 290 | 291 | .bespoke select::-ms-expand { 292 | display: none; 293 | } 294 | 295 | /* Radio Buttons - TODO: Remove when design system adds radio component */ 296 | .bespoke input[type="radio"] { 297 | appearance: none; 298 | width: 1rem; 299 | height: 1rem; 300 | border: 2px solid var(--Colors-Input-Border-Default); 301 | border-radius: 50%; 302 | background: var(--Colors-Input-Background-Default); 303 | cursor: pointer; 304 | position: relative; 305 | transition: all 0.2s ease; 306 | flex-shrink: 0; 307 | padding: 0; 308 | } 309 | 310 | .bespoke input[type="radio"]:checked { 311 | border-color: var(--Colors-Stroke-Primary); 312 | background: var(--Colors-Stroke-Primary); 313 | } 314 | 315 | .bespoke input[type="radio"]:checked::after { 316 | content: ''; 317 | position: absolute; 318 | top: 50%; 319 | left: 50%; 320 | transform: translate(-50%, -50%); 321 | width: 0.375rem; 322 | height: 0.375rem; 323 | border-radius: 50%; 324 | background: white; 325 | } 326 | 327 | .bespoke input[type="radio"]:hover { 328 | border-color: var(--Colors-Input-Border-Hover); 329 | } 330 | 331 | .bespoke input[type="radio"]:focus-visible { 332 | outline: none; 333 | border-color: var(--Colors-Input-Border-Focus); 334 | box-shadow: 0 0 0 3px var(--Colors-Input-Shadow-Focus); 335 | } 336 | 337 | .bespoke .radio-group { 338 | display: flex; 339 | flex-direction: column; 340 | gap: var(--UI-Spacing-spacing-s); 341 | } 342 | 343 | .bespoke .radio-group.horizontal { 344 | flex-direction: row; 345 | align-items: center; 346 | gap: var(--UI-Spacing-spacing-ml); 347 | } 348 | 349 | /* Checkbox - TODO: Remove when design system adds checkbox component */ 350 | .bespoke input[type="checkbox"] { 351 | padding: 0; 352 | margin: 0; 353 | } 354 | 355 | /* Toggle Switch - TODO: Remove when design system adds toggle component */ 356 | .bespoke .toggle { 357 | position: relative; 358 | display: inline-block; 359 | width: 3rem; 360 | height: 1.5rem; 361 | } 362 | 363 | .bespoke .toggle-input { 364 | opacity: 0; 365 | width: 0; 366 | height: 0; 367 | } 368 | 369 | .bespoke .toggle-slider { 370 | position: absolute; 371 | cursor: pointer; 372 | top: 0; 373 | left: 0; 374 | right: 0; 375 | bottom: 0; 376 | background-color: var(--Colors-Stroke-Medium); 377 | transition: 0.3s; 378 | border-radius: 1.5rem; 379 | } 380 | 381 | .bespoke .toggle-slider:before { 382 | position: absolute; 383 | content: ""; 384 | height: 1.125rem; 385 | width: 1.125rem; 386 | left: 0.1875rem; 387 | bottom: 0.1875rem; 388 | background-color: white; 389 | transition: 0.3s; 390 | border-radius: 50%; 391 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 392 | } 393 | 394 | .bespoke .toggle-input:checked + .toggle-slider { 395 | background-color: var(--Colors-Primary-Default); 396 | } 397 | 398 | .bespoke .toggle-input:checked + .toggle-slider:before { 399 | transform: translateX(1.5rem); 400 | } 401 | 402 | .bespoke .toggle-input:focus + .toggle-slider { 403 | box-shadow: 0 0 0 3px var(--Colors-Input-Shadow-Focus); 404 | } 405 | 406 | .bespoke .toggle-input:disabled + .toggle-slider { 407 | opacity: 0.5; 408 | cursor: not-allowed; 409 | } 410 | 411 | .bespoke .toggle-label { 412 | margin-left: var(--UI-Spacing-spacing-s); 413 | font-size: var(--Fonts-Body-Default-xs); 414 | color: var(--Colors-Text-Body-Default); 415 | cursor: pointer; 416 | } 417 | 418 | /* Dark mode adjustments */ 419 | @media (prefers-color-scheme: dark) { 420 | .bespoke .modal-backdrop { 421 | background: rgba(0, 0, 0, 0.7); 422 | } 423 | 424 | .bespoke .modal-body code, 425 | .bespoke .modal-body pre { 426 | background: rgba(148, 163, 184, 0.2); 427 | } 428 | 429 | .bespoke select { 430 | background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23c1c7d7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e"); 431 | } 432 | } 433 | 434 | /* Responsive Design */ 435 | @media (max-width: 768px) { 436 | .bespoke .main-layout { 437 | grid-template-columns: 1fr; 438 | grid-template-rows: auto 1fr; 439 | } 440 | 441 | .bespoke .sidebar { 442 | border-right: none; 443 | border-bottom: 1px solid var(--Colors-Stroke-Default); 444 | } 445 | 446 | .bespoke .modal { 447 | padding: var(--UI-Spacing-spacing-s); 448 | } 449 | 450 | .bespoke .modal-content { 451 | max-height: 95vh; 452 | } 453 | 454 | .bespoke .modal-header { 455 | padding: var(--UI-Spacing-spacing-mxl); 456 | } 457 | 458 | .bespoke .modal-body { 459 | padding: var(--UI-Spacing-spacing-mxl); 460 | } 461 | 462 | .bespoke .modal-header h2 { 463 | font-size: var(--Fonts-Body-Default-lg); 464 | } 465 | } 466 | 467 | -------------------------------------------------------------------------------- /BESPOKE-TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Bespoke Simulation Template 2 | 3 | This document provides precise implementation instructions for creating 4 | embedded applications using the Bespoke Simulation template. Follow these 5 | instructions exactly to ensure consistency across all applications. 6 | NOTE: Never edit this `BESPOKE-TEMPLATE.md` file. Codebase changes should be reflected in the `AGENTS.md` file. 7 | 8 | ## Required Files Structure 9 | 10 | Every application should include these files in the following order: 11 | 12 | 1. CodeSignal Design System foundations: 13 | - colors/colors.css 14 | - spacing/spacing.css 15 | - typography/typography.css 16 | - components/button/button.css (used in header) 17 | 2. CodeSignal Design System components (optional): 18 | - components/boxes/boxes.css 19 | - components/dropdown/dropdown.css 20 | - components/input/input.css 21 | - components/tags/tags.css 22 | 3. bespoke-template.css (template-specific layout, utilities, temporary 23 | components) 24 | 4. help-modal.js (help system) 25 | 5. app.js (application logic) 26 | 6. server.js (server) 27 | 28 | ## HTML Template Implementation 29 | 30 | 1. REPLACE the following placeholders in index.html EXACTLY as specified: 31 | 32 | a) `` 33 | Replace with your application's page title 34 | Example: "Database Designer" or "Task Manager" 35 | 36 | b) `` 37 | Replace with your application's display name (appears in header) 38 | Example: "Database Designer" or "Task Manager" 39 | 40 | c) `` 41 | Add your application's main content area 42 | Example: `
` or `
` 43 | 44 | d) `` 45 | Add links to your application-specific CSS files 46 | Example: `` 47 | 48 | e) `` 49 | Add links to your application-specific JavaScript files 50 | Example: `` 51 | 52 | 2. DO NOT modify the core structure (header, script loading order, etc.) 53 | 54 | ## CSS Implementation 55 | 56 | 1. ALWAYS use the `.bespoke` class on the body element for scoping 57 | 2. USE design system components directly with proper classes: 58 | - Buttons: `button button-primary`, `button button-secondary`, 59 | `button button-danger`, `button button-text` 60 | - Boxes/Cards: `box card` for card containers 61 | - Inputs: Add `input` class to input elements: 62 | `` 63 | 3. USE design system CSS custom properties for styling: 64 | - Colors: `--Colors-*` (e.g., `--Colors-Primary-Default`, 65 | `--Colors-Text-Body-Default`) 66 | - Spacing: `--UI-Spacing-*` (e.g., `--UI-Spacing-spacing-ml`, 67 | `--UI-Spacing-spacing-xl`) 68 | - Typography: `--Fonts-*` (e.g., `--Fonts-Body-Default-md`, 69 | `--Fonts-Headlines-sm`) 70 | - Borders: `--UI-Radius-*` (e.g., `--UI-Radius-radius-s`, 71 | `--UI-Radius-radius-m`) 72 | - Font families: `--body-family`, `--heading-family` 73 | 4. FOR custom styling, create app-specific CSS files 74 | 5. OVERRIDE design system variables in your app-specific CSS, not in 75 | bespoke-template.css 76 | 6. FOLLOW design system naming conventions for consistency 77 | 78 | ## JavaScript Implementation 79 | 80 | 1. HELP MODAL SETUP: 81 | a) Create help content using help-content-template.html as reference 82 | b) Initialize HelpModal with: 83 | - triggerSelector: `'#btn-help'` 84 | - content: your help content (string or loaded from file) 85 | - theme: `'auto'` 86 | 87 | 2. STATUS MANAGEMENT: 88 | a) Use the provided setStatus() function for status updates 89 | b) Update status for: loading, saving, errors, user actions 90 | c) Keep status messages concise and informative 91 | 92 | ## Error Handling Requirements 93 | 94 | 1. WRAP all async operations in try-catch blocks 95 | 2. PROVIDE meaningful error messages to users 96 | 3. LOG errors to console for debugging 97 | 4. IMPLEMENT retry logic for network operations 98 | 5. HANDLE localStorage quota exceeded errors 99 | 6. VALIDATE data before saving operations 100 | 101 | ## Status Message Conventions 102 | 103 | Use these EXACT status messages for consistency: 104 | 105 | - "Ready" - Application loaded successfully 106 | - "Loading..." - Data is being loaded 107 | - "Saving..." - Data is being saved 108 | - "Changes saved" - Auto-save completed successfully 109 | - "Save failed (will retry)" - Server save failed, will retry 110 | - "Failed to load data" - Data loading failed 111 | - "Auto-save initialized" - Auto-save system started 112 | 113 | ## File Naming Conventions 114 | 115 | 1. CSS files: kebab-case (e.g., my-app.css, task-manager.css) 116 | 2. JavaScript files: kebab-case (e.g., my-app.js, task-manager.js) 117 | 3. Data files: kebab-case (e.g., solution.json, initial-data.json) 118 | 4. Image files: kebab-case (e.g., overview.png, help-icon.svg) 119 | 120 | --- 121 | 122 | # Bespoke Template Design System Guidelines 123 | 124 | This section explains how to use the CodeSignal Design System with the 125 | Bespoke template for embedded applications. 126 | 127 | ## Overview 128 | 129 | The Bespoke template uses the CodeSignal Design System for components and 130 | tokens, with template-specific layout and utilities. All styles are scoped 131 | under the `.bespoke` class to prevent interference with parent site styles. 132 | The template uses design system components directly where available, and 133 | provides temporary components (modals, form elements) that will be replaced 134 | when the design system adds them. 135 | 136 | ## Basic Usage 137 | 138 | ### 1. Include the CSS 139 | 140 | ```html 141 | 142 | ``` 143 | 144 | ### 2. Wrap Your Application 145 | 146 | ```html 147 |
148 | 149 |
150 | ``` 151 | 152 | ### 3. Use the Component Classes 153 | 154 | ```html 155 |
156 |
157 |

My App

158 |
Ready
159 | 160 |
161 | 162 |
163 | 174 | 175 |
176 | 177 |
178 |
179 |
180 | ``` 181 | 182 | ## Component Reference 183 | 184 | ### Layout Components 185 | 186 | #### Header 187 | 188 | ```html 189 |
190 |

App Title

191 |
Status message
192 | 193 |
194 | ``` 195 | 196 | #### Main Layout (Sidebar + Content) 197 | 198 | ```html 199 |
200 | 203 |
204 | 205 |
206 |
207 | ``` 208 | 209 | #### Cards 210 | 211 | ```html 212 |
213 |

Card Title

214 |

Subtitle

215 |

Card content goes here

216 |
217 | ``` 218 | 219 | ### Form Components 220 | 221 | #### Labels 222 | 223 | ```html 224 | 225 | 228 | 229 | 230 | 234 | ``` 235 | 236 | #### Input Fields 237 | 238 | ```html 239 | 240 | 241 | 242 | 243 | 247 | 248 | 249 | 250 | 251 | 252 |
253 | 257 | 261 |
262 | 263 | 264 |
265 | 269 | 273 |
274 | 275 | 276 | 277 | 278 | 279 | 286 | ``` 287 | 288 | #### Buttons 289 | 290 | ```html 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | Link Button 301 | ``` 302 | 303 | ### Modal Components 304 | 305 | #### Basic Modal 306 | 307 | ```html 308 | 320 | ``` 321 | 322 | ## Customization 323 | 324 | ### CSS Custom Properties 325 | 326 | You can override any CSS custom property to customize the appearance: 327 | 328 | ```css 329 | .bespoke { 330 | /* Override colors */ 331 | --bespoke-bg: #f0f0f0; 332 | --bespoke-fg: #333333; 333 | --bespoke-accent: #ff6b6b; 334 | 335 | /* Override spacing */ 336 | --bespoke-space-lg: 1.5rem; 337 | 338 | /* Override border radius */ 339 | --bespoke-radius-lg: 12px; 340 | } 341 | ``` 342 | 343 | ### Available Custom Properties 344 | 345 | #### Colors 346 | 347 | - `--bespoke-bg`: Background color 348 | - `--bespoke-fg`: Text color 349 | - `--bespoke-muted`: Muted text color 350 | - `--bespoke-box`: Container/surface background 351 | - `--bespoke-stroke`: Border color 352 | - `--bespoke-danger`: Error/danger color 353 | - `--bespoke-accent`: Accent/primary color 354 | - `--bespoke-control-bg`: Input/button background 355 | - `--bespoke-control-border`: Input/button border 356 | - `--bespoke-control-focus`: Focus ring color 357 | 358 | #### Spacing 359 | 360 | - `--bespoke-space-xs`: 0.25rem 361 | - `--bespoke-space-sm`: 0.5rem 362 | - `--bespoke-space-md`: 0.75rem 363 | - `--bespoke-space-lg`: 1rem 364 | - `--bespoke-space-xl`: 1.5rem 365 | - `--bespoke-space-2xl`: 2rem 366 | 367 | #### Border Radius 368 | 369 | - `--bespoke-radius-sm`: 4px 370 | - `--bespoke-radius-md`: 6px 371 | - `--bespoke-radius-lg`: 8px 372 | - `--bespoke-radius-xl`: 12px 373 | 374 | #### Shadows 375 | 376 | - `--bespoke-shadow-sm`: Small shadow 377 | - `--bespoke-shadow-md`: Medium shadow 378 | - `--bespoke-shadow-lg`: Large shadow 379 | - `--bespoke-shadow-xl`: Extra large shadow 380 | 381 | ## Theme Support 382 | 383 | ### Automatic Dark Mode 384 | 385 | The framework automatically detects the user's system preference and switches 386 | between light and dark themes. No additional configuration is needed. 387 | 388 | ## Integration Examples 389 | 390 | ### Database Designer 391 | 392 | ```html 393 |
394 |
395 |

DB Schema Designer

396 | 397 |
Ready
398 | 399 |
400 | 401 |
402 | 413 | 414 |
415 | 416 |
417 |
418 |
419 | ``` 420 | 421 | ## Best Practices 422 | 423 | 1. **Always wrap in `.bespoke`**: This prevents style conflicts with the parent 424 | site 425 | 2. **Use design system components directly**: Use proper class combinations like 426 | `button button-primary` 427 | 3. **Use semantic HTML**: Combine with proper HTML elements for accessibility 428 | 4. **Customize via design system CSS variables**: Override design system 429 | variables in your app-specific CSS 430 | 5. **Test in both themes**: Ensure your app works in light and dark modes 431 | 6. **Note on temporary components**: Modal and form components in 432 | `bespoke-template.css` are temporary and will be replaced when the design 433 | system adds them 434 | 435 | --------------------------------------------------------------------------------