An easy-to-use platform for EEG experimentation in the classroom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Complete shadcn/ui migration: remove all CSS modules, establish design system

- Delete all 4 CSS module files (common, collect, topnavbar, secondarynav)
- Add brand/accent/signal color tokens to tailwind.config.js
- Update Button default variant to use brand teal instead of blue
- Add shadcn Select and Badge ui primitives; install @radix-ui/react-select
- Migrate all 20 components to shadcn Card, DropdownMenu, Button, Badge
- Replace nav CSS class string props with typed status/active props
- All 25 tests pass, clean build

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

authored by

jdpigeon
Claude Sonnet 4.6
and committed by
Teon L Brooks
06c384c6 27db1cb3

+773 -1285
+17
.llms/learnings.md
··· 8 8 --- 9 9 10 10 <!-- Add entries below this line --> 11 + 12 + ## Styling System (post Phase 4 migration) 13 + 14 + The app uses shadcn/ui + Tailwind CSS. CSS modules have been fully removed. Key conventions: 15 + - **Brand color**: `bg-brand` / `text-brand` (teal `#007c70`) — defined in `tailwind.config.js` 16 + - **Accent color**: `border-accent` (gold `#ffc107`) — used for nav active/visited underline indicators 17 + - **Signal quality colors**: `text-signal-great/ok/bad/none` — defined in Tailwind config 18 + - **shadcn components** in `src/renderer/components/ui/`: Button, Card, Dialog, DropdownMenu, Table, Select, Badge, utils 19 + - **Multi-selects** (`<select multiple>`) use styled native HTML elements since shadcn Select doesn't support multi-select 20 + - **Nav state**: `PrimaryNavSegment` accepts `status: 'active' | 'visited' | 'initial'` (not CSS class strings); `SecondaryNavSegment` accepts `active: boolean` 21 + - **Background gradient** used on all main screens: `bg-gradient-to-b from-[#f9f9f9] to-[#f0f0ff]` 22 + - **`@radix-ui/react-select`** is installed for the shadcn Select component 23 + 24 + ## Pre-existing TypeScript errors (do not treat as regressions) 25 + 26 + - `src/renderer/epics/experimentEpics.ts` (lines 170, 205) — RxJS operator type mismatch 27 + - `src/renderer/routes.tsx` (lines 15-17) — Redux container component prop types
+106
package-lock.json
··· 17 17 "@nteract/transforms": "^3.2.0", 18 18 "@radix-ui/react-dialog": "^1.1.0", 19 19 "@radix-ui/react-dropdown-menu": "^2.1.0", 20 + "@radix-ui/react-select": "^2.2.6", 20 21 "@radix-ui/react-slot": "^1.1.0", 21 22 "@reduxjs/toolkit": "^2.11.2", 22 23 "ajv": "^8.18.0", ··· 3069 3070 "integrity": "sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==", 3070 3071 "license": "MIT" 3071 3072 }, 3073 + "node_modules/@radix-ui/number": { 3074 + "version": "1.1.1", 3075 + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", 3076 + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", 3077 + "license": "MIT" 3078 + }, 3072 3079 "node_modules/@radix-ui/primitive": { 3073 3080 "version": "1.1.3", 3074 3081 "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", ··· 3565 3572 } 3566 3573 } 3567 3574 }, 3575 + "node_modules/@radix-ui/react-select": { 3576 + "version": "2.2.6", 3577 + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", 3578 + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", 3579 + "license": "MIT", 3580 + "dependencies": { 3581 + "@radix-ui/number": "1.1.1", 3582 + "@radix-ui/primitive": "1.1.3", 3583 + "@radix-ui/react-collection": "1.1.7", 3584 + "@radix-ui/react-compose-refs": "1.1.2", 3585 + "@radix-ui/react-context": "1.1.2", 3586 + "@radix-ui/react-direction": "1.1.1", 3587 + "@radix-ui/react-dismissable-layer": "1.1.11", 3588 + "@radix-ui/react-focus-guards": "1.1.3", 3589 + "@radix-ui/react-focus-scope": "1.1.7", 3590 + "@radix-ui/react-id": "1.1.1", 3591 + "@radix-ui/react-popper": "1.2.8", 3592 + "@radix-ui/react-portal": "1.1.9", 3593 + "@radix-ui/react-primitive": "2.1.3", 3594 + "@radix-ui/react-slot": "1.2.3", 3595 + "@radix-ui/react-use-callback-ref": "1.1.1", 3596 + "@radix-ui/react-use-controllable-state": "1.2.2", 3597 + "@radix-ui/react-use-layout-effect": "1.1.1", 3598 + "@radix-ui/react-use-previous": "1.1.1", 3599 + "@radix-ui/react-visually-hidden": "1.2.3", 3600 + "aria-hidden": "^1.2.4", 3601 + "react-remove-scroll": "^2.6.3" 3602 + }, 3603 + "peerDependencies": { 3604 + "@types/react": "*", 3605 + "@types/react-dom": "*", 3606 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3607 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3608 + }, 3609 + "peerDependenciesMeta": { 3610 + "@types/react": { 3611 + "optional": true 3612 + }, 3613 + "@types/react-dom": { 3614 + "optional": true 3615 + } 3616 + } 3617 + }, 3618 + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { 3619 + "version": "1.2.3", 3620 + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", 3621 + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", 3622 + "license": "MIT", 3623 + "dependencies": { 3624 + "@radix-ui/react-compose-refs": "1.1.2" 3625 + }, 3626 + "peerDependencies": { 3627 + "@types/react": "*", 3628 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3629 + }, 3630 + "peerDependenciesMeta": { 3631 + "@types/react": { 3632 + "optional": true 3633 + } 3634 + } 3635 + }, 3568 3636 "node_modules/@radix-ui/react-slot": { 3569 3637 "version": "1.2.4", 3570 3638 "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", ··· 3668 3736 } 3669 3737 } 3670 3738 }, 3739 + "node_modules/@radix-ui/react-use-previous": { 3740 + "version": "1.1.1", 3741 + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", 3742 + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", 3743 + "license": "MIT", 3744 + "peerDependencies": { 3745 + "@types/react": "*", 3746 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3747 + }, 3748 + "peerDependenciesMeta": { 3749 + "@types/react": { 3750 + "optional": true 3751 + } 3752 + } 3753 + }, 3671 3754 "node_modules/@radix-ui/react-use-rect": { 3672 3755 "version": "1.1.1", 3673 3756 "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", ··· 3700 3783 }, 3701 3784 "peerDependenciesMeta": { 3702 3785 "@types/react": { 3786 + "optional": true 3787 + } 3788 + } 3789 + }, 3790 + "node_modules/@radix-ui/react-visually-hidden": { 3791 + "version": "1.2.3", 3792 + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", 3793 + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", 3794 + "license": "MIT", 3795 + "dependencies": { 3796 + "@radix-ui/react-primitive": "2.1.3" 3797 + }, 3798 + "peerDependencies": { 3799 + "@types/react": "*", 3800 + "@types/react-dom": "*", 3801 + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", 3802 + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 3803 + }, 3804 + "peerDependenciesMeta": { 3805 + "@types/react": { 3806 + "optional": true 3807 + }, 3808 + "@types/react-dom": { 3703 3809 "optional": true 3704 3810 } 3705 3811 }
+1
package.json
··· 188 188 "@nteract/transforms": "^3.2.0", 189 189 "@radix-ui/react-dialog": "^1.1.0", 190 190 "@radix-ui/react-dropdown-menu": "^2.1.0", 191 + "@radix-ui/react-select": "^2.2.6", 191 192 "@radix-ui/react-slot": "^1.1.0", 192 193 "@reduxjs/toolkit": "^2.11.2", 193 194 "ajv": "^8.18.0",
+4
src/renderer/app.global.css
··· 5 5 @tailwind components; 6 6 @tailwind utilities; 7 7 8 + :root { 9 + --radius: 0.375rem; 10 + } 11 + 8 12 body { 9 13 position: relative; 10 14 height: 100vh;
+109 -179
src/renderer/components/AnalyzeComponent.tsx
··· 3 3 import { isNil } from 'lodash'; 4 4 import Plot from 'react-plotly.js'; 5 5 import type { Data as PlotlyData } from 'plotly.js'; 6 - import styles from './styles/common.module.css'; 7 6 import { 8 7 DEVICES, 9 8 MUSE_CHANNELS, ··· 26 25 import PyodidePlotWidget from './PyodidePlotWidget'; 27 26 import { HelpButton } from './CollectComponent/HelpSidebar'; 28 27 import { PyodideActions } from '../actions/pyodideActions'; 28 + import { cn } from './ui/utils'; 29 29 30 30 const ANALYZE_STEPS = { 31 31 OVERVIEW: 'OVERVIEW', ··· 45 45 epochsInfo: Array<{ 46 46 [key: string]: number | string; 47 47 }>; 48 - 49 48 channelInfo: Array<string>; 50 - psdPlot: { 51 - [key: string]: string; 52 - }; 53 - 54 - topoPlot: { 55 - [key: string]: string; 56 - }; 57 - 58 - erpPlot: { 59 - [key: string]: string; 60 - }; 61 - 49 + psdPlot: { [key: string]: string }; 50 + topoPlot: { [key: string]: string }; 51 + erpPlot: { [key: string]: string }; 62 52 PyodideActions: typeof PyodideActions; 63 53 } 64 54 65 55 interface State { 66 56 activeStep: string; 67 57 selectedChannel: string; 68 - eegFilePaths: Array<{ 69 - key: string; 70 - text: string; 71 - value: { name: string; dir: string }; 72 - }>; 73 - behaviorFilePaths: Array<{ 74 - key: string; 75 - text: string; 76 - value: string; 77 - }>; 58 + eegFilePaths: Array<{ key: string; text: string; value: { name: string; dir: string } }>; 59 + behaviorFilePaths: Array<{ key: string; text: string; value: string }>; 78 60 selectedFilePaths: Array<string>; 79 61 selectedBehaviorFilePaths: Array<string>; 80 62 selectedSubjects: Array<string>; ··· 82 64 removeOutliers: boolean; 83 65 showDataPoints: boolean; 84 66 isSidebarVisible: boolean; 85 - // TODO: implement outlier display toggle 86 - // displayOutlierVisible: boolean; 87 67 displayMode: string; 88 68 dataToPlot: PlotlyData[]; 89 69 layout: Record<string, any>; 90 70 helpMode: string; 91 - dependentVariables: Array<{ 92 - key: string; 93 - text: string; 94 - value: string; 95 - }>; 71 + dependentVariables: Array<{ key: string; text: string; value: string }>; 96 72 } 97 - // TODO: Add a channel callback from reading epochs so this screen can be aware of which channels are 98 - // available in dataset 99 - // TODO: Refactor component to DRY up handler functions 73 + 100 74 export default class Analyze extends Component<Props, State> { 101 75 constructor(props: Props) { 102 76 super(props); ··· 114 88 removeOutliers: true, 115 89 showDataPoints: false, 116 90 isSidebarVisible: false, 117 - // displayOutlierVisible: false, 118 91 displayMode: 'errorbars', 119 92 helpMode: 'errorbars', 120 93 selectedFilePaths: [], ··· 127 100 }; 128 101 this.handleChannelSelect = this.handleChannelSelect.bind(this); 129 102 this.handleDatasetChange = this.handleDatasetChange.bind(this); 130 - this.handleBehaviorDatasetChange = 131 - this.handleBehaviorDatasetChange.bind(this); 132 - this.handleDependentVariableChange = 133 - this.handleDependentVariableChange.bind(this); 103 + this.handleBehaviorDatasetChange = this.handleBehaviorDatasetChange.bind(this); 104 + this.handleDependentVariableChange = this.handleDependentVariableChange.bind(this); 134 105 this.handleRemoveOutliers = this.handleRemoveOutliers.bind(this); 135 106 this.handleDisplayModeChange = this.handleDisplayModeChange.bind(this); 136 107 this.handleDataPoints = this.handleDataPoints.bind(this); 137 108 this.saveSelectedDatasets = this.saveSelectedDatasets.bind(this); 138 109 this.handleStepClick = this.handleStepClick.bind(this); 139 110 this.handleDropdownClick = this.handleDropdownClick.bind(this); 140 - this.toggleDisplayInfoVisibility = 141 - this.toggleDisplayInfoVisibility.bind(this); 111 + this.toggleDisplayInfoVisibility = this.toggleDisplayInfoVisibility.bind(this); 142 112 } 143 113 144 114 async componentDidMount() { 145 - const workspaceCleanData = await readWorkspaceCleanedEEGData( 146 - this.props.title 147 - ); 115 + const workspaceCleanData = await readWorkspaceCleanedEEGData(this.props.title); 148 116 const behavioralData = await readWorkspaceBehaviorData(this.props.title); 149 117 this.setState({ 150 118 eegFilePaths: workspaceCleanData.map((filepath) => ({ ··· 167 135 } 168 136 169 137 concatSubjectNames = (subjects: Array<string | null | undefined>) => { 170 - if (subjects.length < 1) { 171 - return ''; 172 - } 138 + if (subjects.length < 1) return ''; 173 139 return subjects.reduce((acc, curr) => `${acc}-${curr}`); 174 140 }; 175 141 ··· 266 232 this.state.showDataPoints, 267 233 displayMode 268 234 ); 269 - if (!aggregatedData) { 270 - return; 271 - } 235 + if (!aggregatedData) return; 272 236 const { dataToPlot, layout } = aggregatedData; 273 - this.setState({ 274 - dataToPlot, 275 - layout, 276 - displayMode, 277 - helpMode: displayMode, 278 - }); 237 + this.setState({ dataToPlot, layout, displayMode, helpMode: displayMode }); 279 238 } 280 239 } 281 240 282 241 toggleDisplayInfoVisibility() { 283 - this.setState({ 284 - isSidebarVisible: !this.state.isSidebarVisible, 285 - }); 242 + this.setState({ isSidebarVisible: !this.state.isSidebarVisible }); 286 243 } 287 244 288 245 saveSelectedDatasets() { 289 246 const data = readBehaviorData(this.state.selectedBehaviorFilePaths); 290 - const aggregatedData = aggregateBehaviorDataToSave( 291 - data, 292 - this.state.removeOutliers 293 - ); 247 + const aggregatedData = aggregateBehaviorDataToSave(data, this.state.removeOutliers); 294 248 storeAggregatedBehaviorData(aggregatedData, this.props.title); 295 249 } 296 250 ··· 342 296 return this.renderHelp( 343 297 'Data Points', 344 298 `In this graph, each dot refers to one data point, clustered by group (e.g., conditions). 345 - It’s the most “neutral” way of presenting the data, of course, but it may be difficult to see any patterns. 299 + It's the most "neutral" way of presenting the data, of course, but it may be difficult to see any patterns. 346 300 Why is it always a good idea to look at all your datapoints before interpreting any trends in the data?` 347 301 ); 348 302 case 'errorbars': ··· 360 314 'Box Plot', 361 315 `Box plots summarize the data in a more informative way: 362 316 they actually tell you something about the distribution of datapoints within a group, 363 - by taking the median as its reference point instead of the mean 364 - (the median is the “middle” point in a dataset after sorting it from the lowest to the highest value). 365 - The boxes represent so-called “quartiles” which are cut off at the value right between the median and the smallest value or highest value in the dataset. 366 - The lines (“whiskers”) show how much variability there is in the data outside of those quartiles; 367 - any outliers are shown as individual points. Can you go through each plot and describe exactly what you see? 368 - When you toggle between this view and the bar graph view, do the data look very different?` 317 + by taking the median as its reference point instead of the mean. 318 + The boxes represent so-called "quartiles". 319 + The lines ("whiskers") show how much variability there is in the data outside of those quartiles; 320 + any outliers are shown as individual points.` 369 321 ); 370 322 case 'outliers': 371 323 default: 372 324 return this.renderHelp( 373 325 'Outliers', 374 - `A datapoint is tagged as an “outlier” if its value exceeds 2 standard deviations below or above the mean of all data in the group. 375 - If a datapoint is unusually high or low (it “deviates”) compared to the rest of the group, 376 - it is likely a special case that doesn’t tell us anything informative about the group as a whole. 377 - Removing such outliers can help unskew the data. What might outliers mean in your dataset? 378 - Can you think of any other cases where identifying outliers can be helpful?` 326 + `A datapoint is tagged as an "outlier" if its value exceeds 2 standard deviations below or above the mean of all data in the group. 327 + Removing such outliers can help unskew the data.` 379 328 ); 380 329 } 381 330 } 382 331 383 332 renderHelp(header: string, content: string) { 384 333 return ( 385 - <div className={styles.helpContent}> 334 + <div className="text-lg h-[80%]"> 386 335 <button 387 - className={styles.closeButton} 336 + className="flex justify-end w-full" 388 337 onClick={this.toggleDisplayInfoVisibility} 389 338 aria-label="Close" 390 - >✕</button> 391 - <h1 className={styles.helpHeader}>{header}</h1> 339 + > 340 + 341 + </button> 342 + <h1 className="mb-4">{header}</h1> 392 343 {content} 393 344 </div> 394 345 ); ··· 401 352 return ( 402 353 <> 403 354 <div className="w-1/3 p-2 text-left"> 404 - <div className={styles.infoSegment}> 405 - <h1>Overview</h1> 406 - <p> 407 - Load cleaned datasets from different subjects and view how the 408 - EEG differs between electrodes 409 - </p> 410 - <h4>Select Clean Datasets</h4> 411 - <select 412 - multiple 413 - className="w-full border border-gray-300 rounded p-1" 414 - value={this.state.selectedFilePaths} 415 - onChange={this.handleDatasetChange} 416 - > 417 - {this.state.eegFilePaths.map((eegFilePath) => ( 418 - <option key={eegFilePath.key} value={String(eegFilePath.value)}> 419 - {eegFilePath.text} 420 - </option> 421 - ))} 422 - </select> 423 - {this.renderEpochLabels()} 424 - </div> 355 + <h1>Overview</h1> 356 + <p> 357 + Load cleaned datasets from different subjects and view how the 358 + EEG differs between electrodes 359 + </p> 360 + <h4>Select Clean Datasets</h4> 361 + <select 362 + multiple 363 + className="w-full border border-gray-300 rounded p-1" 364 + value={this.state.selectedFilePaths} 365 + onChange={this.handleDatasetChange} 366 + > 367 + {this.state.eegFilePaths.map((eegFilePath) => ( 368 + <option key={eegFilePath.key} value={String(eegFilePath.value)}> 369 + {eegFilePath.text} 370 + </option> 371 + ))} 372 + </select> 373 + {this.renderEpochLabels()} 425 374 </div> 426 375 <div className="w-2/3 p-2"> 427 376 <PyodidePlotWidget 428 377 title={this.props.title} 429 - imageTitle={`${this.concatSubjectNames( 430 - this.state.selectedSubjects 431 - )}-Topoplot`} 378 + imageTitle={`${this.concatSubjectNames(this.state.selectedSubjects)}-Topoplot`} 432 379 plotMIMEBundle={this.props.topoPlot} 433 380 /> 434 381 </div> ··· 437 384 case ANALYZE_STEPS.ERP: 438 385 return ( 439 386 <> 440 - <div className={['w-1/3 p-2 text-left', styles.analyzeColumn].join(' ')}> 441 - <div className={styles.infoSegment}> 442 - <h1>ERP</h1> 443 - <p> 444 - The event-related potential represents EEG activity elicited 445 - by a particular sensory event 446 - </p> 447 - <ClickableHeadDiagramSVG 448 - channelinfo={this.props.channelInfo} 449 - onChannelClick={this.handleChannelSelect} 450 - /> 451 - <div className="my-2" /> 452 - {this.renderEpochLabels()} 453 - </div> 387 + <div className="w-1/3 p-2 text-left h-full"> 388 + <h1>ERP</h1> 389 + <p> 390 + The event-related potential represents EEG activity elicited 391 + by a particular sensory event 392 + </p> 393 + <ClickableHeadDiagramSVG 394 + channelinfo={this.props.channelInfo} 395 + onChannelClick={this.handleChannelSelect} 396 + /> 397 + <div className="my-2" /> 398 + {this.renderEpochLabels()} 454 399 </div> 455 400 <div className="w-2/3 p-2"> 456 401 <PyodidePlotWidget 457 402 title={this.props.title} 458 - imageTitle={`${this.concatSubjectNames( 459 - this.state.selectedSubjects 460 - )}-${this.state.selectedChannel}-ERP`} 403 + imageTitle={`${this.concatSubjectNames(this.state.selectedSubjects)}-${this.state.selectedChannel}-ERP`} 461 404 plotMIMEBundle={this.props.erpPlot} 462 405 /> 463 406 </div> ··· 467 410 return ( 468 411 <> 469 412 <div className="w-1/3 p-2 text-left"> 470 - <div className={styles.infoSegment}> 471 - <h1>Overview</h1> 472 - <p> 473 - Load datasets from different subjects and view behavioral results 474 - </p> 475 - <div className="flex items-center justify-between mb-2"> 476 - <span className="font-semibold">Datasets</span> 477 - <Button variant="outline" size="sm" onClick={this.saveSelectedDatasets}> 478 - ↓ Export 479 - </Button> 480 - </div> 481 - <select 482 - multiple 483 - className="w-full border border-gray-300 rounded p-1" 484 - value={this.state.selectedBehaviorFilePaths} 485 - onChange={this.handleBehaviorDatasetChange} 486 - onClick={this.handleDropdownClick} 487 - > 488 - {this.state.behaviorFilePaths.map((fp) => ( 489 - <option key={fp.key} value={fp.value}>{fp.text}</option> 490 - ))} 491 - </select> 492 - <div className="my-2" /> 493 - <p className="font-semibold">Dependent Variable</p> 494 - <select 495 - className="w-full border border-gray-300 rounded p-1" 496 - value={this.state.selectedDependentVariable} 497 - onChange={this.handleDependentVariableChange} 498 - > 499 - {this.state.dependentVariables.map((dv) => ( 500 - <option key={dv.key} value={dv.value}>{dv.text}</option> 501 - ))} 502 - </select> 413 + <h1>Overview</h1> 414 + <p> 415 + Load datasets from different subjects and view behavioral results 416 + </p> 417 + <div className="flex items-center justify-between mb-2"> 418 + <span className="font-semibold">Datasets</span> 419 + <Button variant="outline" size="sm" onClick={this.saveSelectedDatasets}> 420 + ↓ Export 421 + </Button> 503 422 </div> 423 + <select 424 + multiple 425 + className="w-full border border-gray-300 rounded p-1" 426 + value={this.state.selectedBehaviorFilePaths} 427 + onChange={this.handleBehaviorDatasetChange} 428 + onClick={this.handleDropdownClick} 429 + > 430 + {this.state.behaviorFilePaths.map((fp) => ( 431 + <option key={fp.key} value={fp.value}>{fp.text}</option> 432 + ))} 433 + </select> 434 + <div className="my-2" /> 435 + <p className="font-semibold">Dependent Variable</p> 436 + <select 437 + className="w-full border border-gray-300 rounded p-1" 438 + value={this.state.selectedDependentVariable} 439 + onChange={this.handleDependentVariableChange} 440 + > 441 + {this.state.dependentVariables.map((dv) => ( 442 + <option key={dv.key} value={dv.value}>{dv.text}</option> 443 + ))} 444 + </select> 504 445 </div> 505 - <div 506 - className="w-2/3 p-2" 507 - style={{ overflow: 'auto', maxHeight: 650 }} 508 - > 509 - <div className={['text-left', styles.plotSegment].join(' ')}> 446 + <div className="w-2/3 p-2" style={{ overflow: 'auto', maxHeight: 650 }}> 447 + <div className="text-left"> 510 448 <Plot data={this.state.dataToPlot} layout={this.state.layout} /> 511 449 <div className="my-2" /> 512 450 <label className="flex items-center gap-2"> ··· 519 457 </label> 520 458 <div className="my-2" /> 521 459 <div className="flex gap-1"> 522 - <button 523 - className={['px-3 py-1 border rounded', this.state.displayMode === 'datapoints' ? 'bg-gray-200' : ''].join(' ')} 524 - onClick={() => this.handleDisplayModeChange('datapoints')} 525 - > 526 - Data Points 527 - </button> 528 - <button 529 - className={['px-3 py-1 border rounded', this.state.displayMode === 'errorbars' ? 'bg-gray-200' : ''].join(' ')} 530 - onClick={() => this.handleDisplayModeChange('errorbars')} 531 - > 532 - Bar Graph 533 - </button> 534 - <button 535 - className={['px-3 py-1 border rounded', this.state.displayMode === 'whiskers' ? 'bg-gray-200' : ''].join(' ')} 536 - onClick={() => this.handleDisplayModeChange('whiskers')} 537 - > 538 - Box Plot 539 - </button> 460 + {(['datapoints', 'errorbars', 'whiskers'] as const).map((mode) => ( 461 + <Button 462 + key={mode} 463 + variant={this.state.displayMode === mode ? 'secondary' : 'outline'} 464 + size="sm" 465 + onClick={() => this.handleDisplayModeChange(mode)} 466 + > 467 + {mode === 'datapoints' ? 'Data Points' : mode === 'errorbars' ? 'Bar Graph' : 'Box Plot'} 468 + </Button> 469 + ))} 540 470 </div> 541 471 <HelpButton onClick={this.toggleDisplayInfoVisibility} /> 542 472 {this.state.isSidebarVisible && ( 543 - <div className={styles.helpSidebar}> 473 + <div className="h-full"> 544 474 {this.renderHelpContent()} 545 475 </div> 546 476 )} ··· 553 483 554 484 render() { 555 485 return ( 556 - <div className={styles.mainContainer}> 486 + <div className="h-screen p-[3%] bg-gradient-to-b from-[#f9f9f9] to-[#f0f0ff]"> 557 487 <SecondaryNavComponent 558 488 title="Analyze" 559 489 steps={ ··· 564 494 activeStep={this.state.activeStep} 565 495 onStepClick={this.handleStepClick} 566 496 /> 567 - <div className={['flex items-start', styles.contentGrid].join(' ')}> 497 + <div className="flex items-start h-[90%]"> 568 498 {this.renderSectionContent()} 569 499 </div> 570 500 </div>
+21 -12
src/renderer/components/CleanComponent/CleanSidebar.tsx
··· 1 1 import React, { Component } from 'react'; 2 2 import { Button } from '../ui/button'; 3 - import styles from '../styles/common.module.css'; 4 3 5 4 enum HELP_STEP { 6 5 MENU = 0, ··· 21 20 interface State { 22 21 helpStep: HELP_STEP; 23 22 } 23 + 24 24 export default class CleanSidebar extends Component<Props, State> { 25 25 constructor(props) { 26 26 super(props); ··· 59 59 renderMenu() { 60 60 return ( 61 61 <div className="flex flex-col"> 62 - <h1 className={styles.helpHeader}>What would you like to do?</h1> 63 - <div className={styles.helpMenuItem} onClick={this.handleStartSignal}> 62 + <h1 className="mb-4">What would you like to do?</h1> 63 + <div 64 + className="text-lg p-1 cursor-pointer hover:bg-gray-100" 65 + onClick={this.handleStartSignal} 66 + > 64 67 ★ Improve the signal quality of your sensors 65 68 </div> 66 - <div className={styles.helpMenuItem} onClick={this.handleStartLearn}> 69 + <div 70 + className="text-lg p-1 cursor-pointer hover:bg-gray-100" 71 + onClick={this.handleStartLearn} 72 + > 67 73 ⚠ Learn about how the subjects movements create noise 68 74 </div> 69 75 </div> ··· 73 79 renderHelp(header: string, content: string) { 74 80 return ( 75 81 <> 76 - <div className={styles.helpContent}> 77 - <h1 className={styles.helpHeader}>{header}</h1> 82 + <div className="text-lg h-[80%]"> 83 + <h1 className="mb-4">{header}</h1> 78 84 {content} 79 85 </div> 80 86 <div className="flex gap-2 mt-4"> ··· 139 145 140 146 render() { 141 147 return ( 142 - <div className={styles.helpSidebar}> 143 - <button 144 - className={styles.closeButton} 145 - onClick={this.props.handleClose} 146 - aria-label="Close" 147 - >✕</button> 148 + <div className="h-full p-4 bg-white border-l border-gray-200"> 149 + <div className="flex justify-end"> 150 + <button 151 + onClick={this.props.handleClose} 152 + aria-label="Close" 153 + > 154 + 155 + </button> 156 + </div> 148 157 {this.renderHelpContent()} 149 158 </div> 150 159 );
+4 -10
src/renderer/components/CleanComponent/index.tsx
··· 3 3 import { Link } from 'react-router-dom'; 4 4 import { isNil, isString } from 'lodash'; 5 5 import { Button } from '../ui/button'; 6 - import styles from '../styles/collect.module.css'; 7 - import commonStyles from '../styles/common.module.css'; 8 6 import { EXPERIMENTS, DEVICES } from '../../constants/constants'; 9 7 import { readWorkspaceRawEEGData } from '../../utils/filesystem/storage'; 10 8 import CleanSidebar from './CleanSidebar'; ··· 73 71 if (acc.find((subject) => subject.key === curr)) { 74 72 return acc; 75 73 } 76 - return acc.concat({ 77 - key: curr, 78 - text: curr, 79 - value: curr, 80 - }); 74 + return acc.concat({ key: curr, text: curr, value: curr }); 81 75 }, []), 82 76 eegFilePaths: workspaceRawData.map((filepath) => ({ 83 77 key: filepath.name, ··· 154 148 }); 155 149 156 150 return ( 157 - <div className={['relative flex', styles.preTestPushable].join(' ')}> 151 + <div className="relative flex h-screen bg-gradient-to-b from-[#f9f9f9] to-[#f0f0ff]"> 158 152 {this.state.isSidebarVisible && ( 159 153 <div className="absolute right-0 top-0 h-full w-64 z-10"> 160 154 <CleanSidebar handleClose={this.handleSidebarToggle} /> 161 155 </div> 162 156 )} 163 - <div className={['flex-1', styles.preTestContainer].join(' ')}> 157 + <div className="flex-1 p-[3%]"> 164 158 <div className="flex items-center mb-4"> 165 159 <h1>Clean</h1> 166 160 </div> 167 161 <div className="flex gap-4"> 168 - <div className={['w-6/12 text-left', commonStyles.infoSegment].join(' ')}> 162 + <div className="w-6/12 text-left"> 169 163 <h1>Select & Clean</h1> 170 164 <p> 171 165 Ready to clean some data? Select a subject and one or more
+14 -20
src/renderer/components/CollectComponent/ConnectModal.tsx
··· 9 9 CONNECTION_STATUS, 10 10 SCREENS, 11 11 } from '../../constants/constants'; 12 - import styles from '../styles/collect.module.css'; 13 12 import { SignalQualityData } from '../../constants/interfaces'; 14 13 import { DeviceActions } from '../../actions'; 15 14 ··· 37 36 } 38 37 39 38 export default class ConnectModal extends Component<Props, State> { 40 - // handleSearch: () => void; 41 - 42 - // handleStartTutorial: () => void; 43 39 static getDeviceName(device: any) { 44 40 if (!isNil(device)) { 45 41 return isNil(device.name) ? device.id : device.name; ··· 81 77 82 78 handleSearch() { 83 79 this.setState({ instructionProgress: 0 }); 84 - this.props.DeviceActions.SetDeviceAvailability( 85 - DEVICE_AVAILABILITY.SEARCHING 86 - ); 80 + this.props.DeviceActions.SetDeviceAvailability(DEVICE_AVAILABILITY.SEARCHING); 87 81 } 88 82 89 83 handleConnect() { ··· 102 96 {this.props.availableDevices.map((device) => ( 103 97 <li 104 98 key={device.id} 105 - className={[styles.deviceItem, 'flex items-center gap-2 py-2 cursor-pointer'].join(' ')} 99 + className="flex items-center gap-2 py-2 cursor-pointer text-lg" 106 100 onClick={() => this.setState({ selectedDevice: device })} 107 101 > 108 102 <span>{this.state.selectedDevice === device ? '✓' : '○'}</span> ··· 116 110 renderContent() { 117 111 if (this.props.deviceAvailability === DEVICE_AVAILABILITY.SEARCHING) { 118 112 return ( 119 - <p className={styles.searchingText}> 113 + <p className="text-center"> 120 114 Searching for available headset(s)... 121 115 </p> 122 116 ); 123 117 } 124 118 if (this.props.connectionStatus === CONNECTION_STATUS.CONNECTING) { 125 119 return ( 126 - <p className={styles.searchingText}> 120 + <p className="text-center"> 127 121 Connecting to {ConnectModal.getDeviceName(this.state.selectedDevice)}... 128 122 </p> 129 123 ); ··· 131 125 if (this.state.instructionProgress === INSTRUCTION_PROGRESS.TURN_ON) { 132 126 return ( 133 127 <> 134 - <h2 className={styles.connectHeader}>Turn your headset on</h2> 128 + <h2>Turn your headset on</h2> 135 129 <p> 136 130 Make sure your headset is on and fully charged. 137 131 </p> ··· 143 137 {(this.state.instructionProgress as number) !== 0 && ( 144 138 <Button 145 139 variant="secondary" 146 - className={['w-full', styles.secondaryButton].join(' ')} 140 + className="w-full" 147 141 onClick={() => this.handleinstructionProgress(0)} 148 142 > 149 143 Back ··· 151 145 )} 152 146 <Button 153 147 variant="default" 154 - className={['w-full', styles.primaryButton].join(' ')} 148 + className="w-full" 155 149 onClick={() => 156 150 this.handleinstructionProgress( 157 151 INSTRUCTION_PROGRESS.COMPUTER_CONNECTABILITY ··· 170 164 ) { 171 165 return ( 172 166 <> 173 - <h2 className={styles.connectHeader}>Insert the USB Receiver</h2> 167 + <h2>Insert the USB Receiver</h2> 174 168 <p> 175 169 Insert the USB receiver into a USB port on your computer. Ensure 176 170 that the LED on the receiver is continously lit or flickering ··· 180 174 <div className="flex gap-2 mt-4"> 181 175 <Button 182 176 variant="secondary" 183 - className={['w-full', styles.secondaryButton].join(' ')} 177 + className="w-full" 184 178 onClick={() => 185 179 this.handleinstructionProgress(INSTRUCTION_PROGRESS.TURN_ON) 186 180 } ··· 189 183 </Button> 190 184 <Button 191 185 variant="default" 192 - className={['w-full', styles.primaryButton].join(' ')} 186 + className="w-full" 193 187 onClick={this.handleSearch} 194 188 > 195 189 Next ··· 201 195 if (this.props.deviceAvailability === DEVICE_AVAILABILITY.AVAILABLE) { 202 196 return ( 203 197 <> 204 - <h2 className={styles.connectHeader}>Headset(s) found</h2> 198 + <h2>Headset(s) found</h2> 205 199 <p>Please select which headset you would like to connect.</p> 206 200 {this.renderAvailableDeviceList()} 207 201 <div className="flex gap-2 mt-4"> 208 202 <Button 209 203 variant="secondary" 210 - className={['w-full', styles.secondaryButton].join(' ')} 204 + className="w-full" 211 205 onClick={() => this.handleinstructionProgress(1)} 212 206 > 213 207 Back 214 208 </Button> 215 209 <Button 216 210 variant="default" 217 - className={['w-full', styles.primaryButton].join(' ')} 211 + className="w-full" 218 212 disabled={isNil(this.state.selectedDevice)} 219 213 onClick={this.handleConnect} 220 214 > ··· 238 232 render() { 239 233 return ( 240 234 <Dialog open={this.props.open} onOpenChange={(open) => { if (!open) this.props.onClose(); }}> 241 - <DialogContent className={styles.connectModal}> 235 + <DialogContent className="max-w-sm text-center"> 242 236 {this.renderContent()} 243 237 </DialogContent> 244 238 </Dialog>
+20 -11
src/renderer/components/CollectComponent/HelpSidebar.tsx
··· 1 1 import React, { Component } from 'react'; 2 2 import { Button } from '../ui/button'; 3 - import styles from '../styles/common.module.css'; 4 3 5 4 enum HELP_STEP { 6 5 MENU, ··· 24 23 25 24 // TODO: Refactor this into a more reusable Sidebar component that can be used in Collect, Clean, and Analyze screen 26 25 export class HelpSidebar extends Component<Props, State> { 27 - // props: Props; 28 - 29 26 constructor(props) { 30 27 super(props); 31 28 this.state = { ··· 69 66 renderMenu() { 70 67 return ( 71 68 <div className="flex flex-col"> 72 - <h1 className={styles.helpHeader}>What would you like to do?</h1> 73 - <div className={styles.helpMenuItem} onClick={this.handleStartSignal}> 69 + <h1 className="mb-4">What would you like to do?</h1> 70 + <div 71 + className="text-lg p-1 cursor-pointer hover:bg-gray-100" 72 + onClick={this.handleStartSignal} 73 + > 74 74 ★ Improve the signal quality of your sensors 75 75 </div> 76 - <div className={styles.helpMenuItem} onClick={this.handleStartLearn}> 76 + <div 77 + className="text-lg p-1 cursor-pointer hover:bg-gray-100" 78 + onClick={this.handleStartLearn} 79 + > 77 80 ⚠ Learn about how the subjects movements create noise 78 81 </div> 79 82 </div> ··· 83 86 renderHelp(header: string, content: string) { 84 87 return ( 85 88 <> 86 - <div className={styles.helpContent}> 87 - <h1 className={styles.helpHeader}>{header}</h1> 89 + <div className="text-lg h-[80%]"> 90 + <h1 className="mb-4">{header}</h1> 88 91 {content} 89 92 </div> 90 93 <div className="flex gap-2 mt-4"> ··· 149 152 150 153 render() { 151 154 return ( 152 - <div className={styles.helpSidebar}> 153 - <div className={styles.closeButton}> 155 + <div className="h-full p-4 bg-white border-l border-gray-200"> 156 + <div className="flex justify-end"> 154 157 <button onClick={this.props.handleClose} aria-label="Close">✕</button> 155 158 </div> 156 159 {this.renderHelpContent()} ··· 161 164 162 165 export const HelpButton: React.FC<{ onClick: () => void }> = ({ onClick }) => { 163 166 return ( 164 - <button className={styles.helpButton} onClick={onClick} aria-label="Help">?</button> 167 + <button 168 + className="h-11 w-11 rounded-full bg-brand text-white flex items-center justify-center font-bold text-lg" 169 + onClick={onClick} 170 + aria-label="Help" 171 + > 172 + ? 173 + </button> 165 174 ); 166 175 };
+7 -8
src/renderer/components/CollectComponent/PreTestComponent.tsx
··· 6 6 import PreviewExperimentComponent from '../PreviewExperimentComponent'; 7 7 import PreviewButton from '../PreviewButtonComponent'; 8 8 import { HelpSidebar, HelpButton } from './HelpSidebar'; 9 - import styles from '../styles/collect.module.css'; 10 9 import { getExperimentFromType } from '../../utils/labjs/functions'; 11 10 import { ExperimentActions, DeviceActions } from '../../actions'; 12 11 import { ··· 102 101 plottingInterval={PLOTTING_INTERVAL} 103 102 /> 104 103 <ul className="mt-2 space-y-1"> 105 - <li><span className={styles.greatSignal}>●</span> Strong Signal</li> 106 - <li><span className={styles.okSignal}>●</span> Mediocre signal</li> 107 - <li><span className={styles.badSignal}>●</span> Weak Signal</li> 108 - <li><span className={styles.noSignal}>●</span> No Signal</li> 104 + <li><span className="text-signal-great">●</span> Strong Signal</li> 105 + <li><span className="text-signal-ok">●</span> Mediocre signal</li> 106 + <li><span className="text-signal-bad">●</span> Weak Signal</li> 107 + <li><span className="text-signal-none">●</span> No Signal</li> 109 108 </ul> 110 109 </div> 111 110 ); ··· 119 118 120 119 render() { 121 120 return ( 122 - <div className={['relative flex', styles.preTestPushable].join(' ')}> 121 + <div className="relative flex h-screen bg-gradient-to-b from-[#f9f9f9] to-[#f0f0ff]"> 123 122 {this.state.isSidebarVisible && ( 124 123 <div className="absolute right-0 top-0 h-full w-64 z-10"> 125 124 <HelpSidebar handleClose={this.handleSidebarToggle} /> 126 125 </div> 127 126 )} 128 - <div className={['flex-1', styles.preTestContainer].join(' ')}> 127 + <div className="flex-1 p-[3%]"> 129 128 <div className="flex items-center justify-between mb-4"> 130 129 <h1>Collect</h1> 131 130 <div className="flex gap-2"> ··· 145 144 </div> 146 145 </div> 147 146 <div className="flex gap-4"> 148 - <div className={['w-1/2', styles.previewEEGWindow].join(' ')}> 147 + <div className="w-1/2 h-full items-center mb-5"> 149 148 {this.renderSignalQualityOrPreview()} 150 149 </div> 151 150 <div className="w-1/2">
+13 -26
src/renderer/components/CollectComponent/RunComponent.tsx
··· 1 1 import React, { useCallback, useState } from 'react'; 2 2 import { Button } from '../ui/button'; 3 3 import { Link } from 'react-router-dom'; 4 - import styles from '../styles/common.module.css'; 5 4 import InputCollect from '../InputCollect'; 6 5 import { injectEmotivMarker } from '../../utils/eeg/emotiv'; 7 6 import { injectMuseMarker } from '../../utils/eeg/muse'; ··· 23 22 deviceType: DEVICES; 24 23 isEEGEnabled: boolean; 25 24 ExperimentActions: typeof globalExperimentActions; 26 - } 27 - 28 - interface State { 29 - isInputCollectOpen: boolean; 30 25 } 31 26 32 27 const Run: React.FC<Props> = ({ ··· 95 90 ); 96 91 97 92 return ( 98 - <div className={styles.mainContainer} data-tid="container"> 99 - <div className={styles.experimentContainer}> 93 + <div className="h-screen p-[3%] bg-gradient-to-b from-[#f9f9f9] to-[#f0f0ff]" data-tid="container"> 94 + <div className="h-full"> 100 95 {!isRunning && ( 101 - <div className={styles.mainContainer}> 102 - <div className={['text-left', styles.descriptionContainer].join(' ')}> 96 + <div className="h-screen p-[3%] bg-gradient-to-b from-[#f9f9f9] to-[#f0f0ff]"> 97 + <div className="text-left"> 103 98 <h1>{title}</h1> 104 99 <button 105 - className={styles.closeButton} 100 + className="flex justify-end w-full" 106 101 onClick={() => setIsInputCollectOpen(true)} 107 102 aria-label="Edit" 108 - >✏</button> 109 - <div className={styles.infoSegment}> 110 - Subject ID: <b>{subject}</b> 111 - </div> 112 - <div className={styles.infoSegment}> 113 - Group Name: <b>{group}</b> 114 - </div> 115 - <div className={styles.infoSegment}> 116 - Session Number: <b>{session}</b> 117 - </div> 103 + > 104 + 105 + </button> 106 + <div>Subject ID: <b>{subject}</b></div> 107 + <div>Group Name: <b>{group}</b></div> 108 + <div>Session Number: <b>{session}</b></div> 118 109 <div className="mt-6"> 119 110 <Button 120 111 variant="default" ··· 130 121 )} 131 122 132 123 {isRunning && ( 133 - <div className={styles.experimentWindow}> 124 + <div className="h-full w-full"> 134 125 <ExperimentWindow 135 126 title={title} 136 127 experimentObject={experimentObject} ··· 146 137 onClose={handleCloseInputCollect} 147 138 onExit={() => setIsInputCollectOpen(false)} 148 139 header="Enter Data" 149 - data={{ 150 - subject, 151 - group, 152 - session, 153 - }} 140 + data={{ subject, group, session }} 154 141 /> 155 142 </div> 156 143 );
+16 -24
src/renderer/components/DesignComponent/CustomDesignComponent.tsx
··· 3 3 import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../ui/table'; 4 4 import { isString } from 'lodash'; 5 5 6 - import styles from '../styles/common.module.css'; 7 6 import { SCREENS } from '../../constants/constants'; 8 7 import { ExperimentParameters } from '../../constants/interfaces'; 9 8 import { DesignProps } from './index'; ··· 114 113 case CUSTOM_STEPS.OVERVIEW: 115 114 default: 116 115 return ( 117 - <div className={['flex gap-4 p-4', styles.contentGrid].join(' ')}> 116 + <div className="flex gap-4 p-4 h-[90%]"> 118 117 <div className="flex-1 flex flex-col items-center"> 119 - <img src={researchQuestionImage} className={styles.overviewImage} alt="Research Question" /> 118 + <img src={researchQuestionImage} className="h-[140px] w-auto" alt="Research Question" /> 120 119 <label className="block text-sm font-medium mb-1">{FIELDS.QUESTION}</label> 121 120 <textarea 122 121 style={{ minHeight: 100, maxHeight: 400 }} ··· 127 126 /> 128 127 </div> 129 128 <div className="flex-1 flex flex-col items-center"> 130 - <img src={hypothesisImage} className={styles.overviewImage} alt="Hypothesis" /> 129 + <img src={hypothesisImage} className="h-[140px] w-auto" alt="Hypothesis" /> 131 130 <label className="block text-sm font-medium mb-1">{FIELDS.HYPOTHESIS}</label> 132 131 <textarea 133 132 style={{ minHeight: 100, maxHeight: 400 }} ··· 138 137 /> 139 138 </div> 140 139 <div className="flex-1 flex flex-col items-center"> 141 - <img src={methodsImage} className={styles.overviewImage} alt="Methods" /> 140 + <img src={methodsImage} className="h-[140px] w-auto" alt="Methods" /> 142 141 <label className="block text-sm font-medium mb-1">{FIELDS.METHODS}</label> 143 142 <textarea 144 143 style={{ minHeight: 100, maxHeight: 400 }} ··· 165 164 program or on one of the websites online.`} 166 165 </p> 167 166 </div> 168 - 169 167 <Table> 170 168 <TableHeader> 171 - <TableRow className={styles.conditionHeaderRow}> 172 - <TableHead className={styles.conditionHeaderRowName}> 173 - Condition 174 - </TableHead> 169 + <TableRow> 170 + <TableHead className="pl-[60px]">Condition</TableHead> 175 171 <TableHead>Default Key Response</TableHead> 176 172 <TableHead>Condition Folder</TableHead> 177 173 </TableRow> 178 174 </TableHeader> 179 - 180 - <TableBody className={styles.experimentTable}> 175 + <TableBody> 181 176 <TableRow> 182 177 <TableCell colSpan={3}>Stimulus customization is currently unavailable</TableCell> 183 178 </TableRow> ··· 194 189 case CUSTOM_STEPS.TRIALS: 195 190 return ( 196 191 <div className="p-4"> 197 - <div className={styles.trialsHeader}> 192 + <div className="grid grid-cols-[auto_1fr] w-full"> 198 193 <div> 199 194 <h1>Trials</h1> 200 195 <p>Edit the correct key response and type of each trial.</p> 201 196 </div> 202 - <div className={styles.trialsTopInfoBar} style={{ alignSelf: 'flex-end' }}> 197 + <div className="grid grid-cols-3 gap-2.5 self-end justify-self-end"> 203 198 <div> 204 199 <label className="block text-sm mb-1">Order</label> 205 200 <select ··· 236 231 </div> 237 232 </div> 238 233 </div> 239 - 240 234 <Table> 241 235 <TableHeader> 242 - <TableRow className={styles.trialsHeaderRow}> 243 - <TableHead className={styles.conditionHeaderRowName}>Name</TableHead> 236 + <TableRow> 237 + <TableHead className="pl-[60px]">Name</TableHead> 244 238 <TableHead>Condition</TableHead> 245 239 <TableHead>Correct Key Response</TableHead> 246 240 <TableHead>Trial Type</TableHead> 247 241 </TableRow> 248 242 </TableHeader> 249 - <TableBody className={styles.trialsTable}> 243 + <TableBody className="overflow-y-scroll max-h-[50vh] block"> 250 244 <TableRow> 251 245 <TableCell colSpan={4}>Stimulus customization is currently unavailable</TableCell> 252 246 </TableRow> ··· 277 271 /> 278 272 </div> 279 273 </div> 280 - 281 274 <div className="w-1/2 flex flex-col justify-between"> 282 275 <div> 283 276 <h1>Image duration</h1> ··· 331 324 }} 332 325 /> 333 326 </div> 334 - 335 327 <div className="w-1/2"> 336 328 <h1>Instructions for the task screen</h1> 337 329 <p>Edit the instruction that will be displayed in the footer during the task.</p> ··· 352 344 353 345 case CUSTOM_STEPS.PREVIEW: 354 346 return ( 355 - <div className={['flex items-start p-4', styles.contentGrid].join(' ')}> 356 - <div className={['flex-1', styles.previewWindow].join(' ')}> 347 + <div className="flex items-start p-4 h-[90%]"> 348 + <div className="flex-1 h-full border border-brand rounded"> 357 349 {this.props.type && ( 358 350 <PreviewExperimentComponent 359 351 isPreviewing={this.state.isPreviewing} ··· 378 370 379 371 render() { 380 372 return ( 381 - <div className={styles.mainContainer}> 373 + <div className="h-screen p-[3%] bg-gradient-to-b from-[#f9f9f9] to-[#f0f0ff]"> 382 374 <SecondaryNavComponent 383 375 title="Experiment Design" 384 376 steps={CUSTOM_STEPS} ··· 389 381 type="checkbox" 390 382 defaultChecked={this.props.isEEGEnabled} 391 383 onChange={(event) => this.handleEEGEnabled(event)} 392 - className={styles.EEGToggle} 384 + className="scale-75" 393 385 /> 394 386 } 395 387 saveButton={
+1 -2
src/renderer/components/DesignComponent/ParamSlider.tsx
··· 1 1 import React from 'react'; 2 2 import Slider from 'rc-slider'; 3 - import styles from '../styles/common.module.css'; 4 3 5 4 interface Props { 6 5 value: number; ··· 19 18 }) => { 20 19 return ( 21 20 <div> 22 - <p className={styles.label}>{label}</p> 21 + <p className="text-sm font-bold leading-[17px]">{label}</p> 23 22 <div className="py-2"> 24 23 {label !== 'Practice trials' || Object.keys(marks).length > 1 ? ( 25 24 <Slider
+18 -21
src/renderer/components/DesignComponent/StimuliDesignColumn.tsx
··· 9 9 import { readImages } from '../../utils/filesystem/storage'; 10 10 import { loadFromSystemDialog } from '../../utils/filesystem/select'; 11 11 import { FILE_TYPES } from '../../constants/constants'; 12 - import styles from '../styles/common.module.css'; 13 12 14 13 interface Props { 15 14 num: number; ··· 58 57 if (images.length < 1) { 59 58 toast.error('No images in folder!'); 60 59 } 61 - this.setState({ 62 - numberImages: images.length, 63 - }); 60 + this.setState({ numberImages: images.length }); 64 61 this.props.onChange('dir', dir, `stimulus${this.props.num}`); 65 62 } 66 63 } 67 64 68 65 handleRemoveFolder() { 69 - this.setState({ 70 - numberImages: 0, 71 - }); 66 + this.setState({ numberImages: 0 }); 72 67 this.props.onChange('dir', '', `stimulus${this.props.num}`); 73 68 } 74 69 75 70 render() { 76 71 return ( 77 - <TableRow className={styles.conditionRow}> 78 - <TableCell className={styles.conditionsNameRow}> 79 - {this.props.num} 80 - <input 81 - className="border border-gray-300 rounded px-2 py-1 w-full mt-1" 82 - value={this.props.title} 83 - onChange={(event) => 84 - this.props.onChange('title', event.target.value, `stimulus${this.props.num}`) 85 - } 86 - placeholder="Enter condition name" 87 - /> 72 + <TableRow> 73 + <TableCell className="pl-[60px]"> 74 + <div className="grid grid-cols-[50px_1fr] items-center gap-2"> 75 + <span>{this.props.num}</span> 76 + <input 77 + className="border border-gray-300 rounded px-2 py-1 w-full" 78 + value={this.props.title} 79 + onChange={(event) => 80 + this.props.onChange('title', event.target.value, `stimulus${this.props.num}`) 81 + } 82 + placeholder="Enter condition name" 83 + /> 84 + </div> 88 85 </TableCell> 89 86 90 - <TableCell className={styles.experimentRowName}> 87 + <TableCell className="pl-6 pr-2.5"> 91 88 <select 92 89 className="w-full border border-gray-300 rounded px-2 py-1" 93 90 value={this.props.response} ··· 105 102 </select> 106 103 </TableCell> 107 104 108 - <TableCell className={styles.experimentRowName}> 105 + <TableCell className="pl-6 pr-2.5"> 109 106 {this.props.dir ? ( 110 - <div className={styles.selectedFolderContainer}> 107 + <div className="inline-grid grid-cols-[auto_auto_1fr] gap-2.5 border-2 border-gray-300 p-2 rounded w-fit items-center"> 111 108 <div> 112 109 Folder{' '} 113 110 {this.props.dir && this.props.dir.split(path.sep).slice(-1).join(' / ')}
+35 -33
src/renderer/components/DesignComponent/StimuliRow.tsx
··· 3 3 import React, { useState } from 'react'; 4 4 import { isString } from 'lodash'; 5 5 import { Button } from '../ui/button'; 6 + import { Badge } from '../ui/badge'; 6 7 import { TableRow, TableCell } from '../ui/table'; 7 - import styles from '../styles/common.module.css'; 8 + import { 9 + DropdownMenu, 10 + DropdownMenuContent, 11 + DropdownMenuItem, 12 + DropdownMenuTrigger, 13 + } from '../ui/dropdown-menu'; 8 14 9 15 interface Props { 10 16 name: string; ··· 33 39 onChange, 34 40 onDelete, 35 41 }) => { 36 - const [phaseMenuOpen, setPhaseMenuOpen] = useState(false); 37 - 38 42 return ( 39 - <TableRow className={styles.trialsRow}> 40 - <TableCell className={styles.conditionsNameRow}> 41 - <div style={{ alignSelf: 'center' }}>{num + 1}.</div> 42 - <div>{name}</div> 43 + <TableRow> 44 + <TableCell className="pl-6 pr-2.5"> 45 + <div className="grid grid-cols-[50px_1fr] items-center gap-2"> 46 + <span>{num + 1}.</span> 47 + <span>{name}</span> 48 + </div> 43 49 </TableCell> 44 50 45 - <TableCell className={styles.experimentRowName}> 51 + <TableCell className="pl-6 pr-2.5"> 46 52 <div>{condition}</div> 47 53 </TableCell> 48 54 49 - <TableCell className={styles.experimentRowName}> 55 + <TableCell className="pl-6 pr-2.5"> 50 56 <select 51 57 className="w-full border border-gray-300 rounded px-2 py-1" 52 58 value={response} ··· 62 68 </select> 63 69 </TableCell> 64 70 65 - <TableCell className={styles.trialsTrialTypeRow}> 66 - <div className={styles.trialsTrialTypeSegment}> 67 - <div 68 - className={styles.trialsTrialTypeRowSelector} 69 - style={{ backgroundColor: phase === 'main' ? '#1AC4EF' : '#EB1B66' }} 70 - > 71 - {phase === 'main' ? 'Experimental' : 'Practice'} 72 - </div> 73 - <div style={{ position: 'relative', display: 'inline-block' }}> 74 - <button 75 - style={{ color: '#C4C4C4' }} 76 - onClick={() => setPhaseMenuOpen((o) => !o)} 77 - > 78 - 79 - </button> 80 - {phaseMenuOpen && ( 81 - <div style={{ position: 'absolute', right: 0, zIndex: 10, background: 'white', border: '1px solid #eee', borderRadius: 4 }}> 82 - <div className="px-3 py-1 cursor-pointer hover:bg-gray-100" onClick={() => { onChange(num, 'phase', 'main'); setPhaseMenuOpen(false); }}>Experimental</div> 83 - <div className="px-3 py-1 cursor-pointer hover:bg-gray-100" onClick={() => { onChange(num, 'phase', 'practice'); setPhaseMenuOpen(false); }}>Practice</div> 84 - </div> 85 - )} 71 + <TableCell className="pl-6 pr-2.5"> 72 + <div className="flex items-center gap-2"> 73 + <div className="grid grid-cols-[auto_1fr] items-center border-2 border-gray-300 rounded px-3 py-2 gap-2"> 74 + <Badge variant={phase === 'main' ? 'experimental' : 'practice'}> 75 + {phase === 'main' ? 'Experimental' : 'Practice'} 76 + </Badge> 77 + <DropdownMenu> 78 + <DropdownMenuTrigger className="text-gray-400 focus:outline-none">▾</DropdownMenuTrigger> 79 + <DropdownMenuContent> 80 + <DropdownMenuItem onClick={() => onChange(num, 'phase', 'main')}> 81 + Experimental 82 + </DropdownMenuItem> 83 + <DropdownMenuItem onClick={() => onChange(num, 'phase', 'practice')}> 84 + Practice 85 + </DropdownMenuItem> 86 + </DropdownMenuContent> 87 + </DropdownMenu> 86 88 </div> 89 + <Button variant="secondary" onClick={() => onDelete(num)}> 90 + Delete 91 + </Button> 87 92 </div> 88 - <Button variant="secondary" onClick={() => onDelete(num)}> 89 - Delete 90 - </Button> 91 93 </TableCell> 92 94 </TableRow> 93 95 );
+23 -42
src/renderer/components/DesignComponent/index.tsx
··· 2 2 import { Button } from '../ui/button'; 3 3 import { isNil } from 'lodash'; 4 4 import { toast } from 'react-toastify'; 5 - import styles from '../styles/common.module.css'; 6 5 import { EXPERIMENTS, SCREENS } from '../../constants/constants'; 7 6 import { readWorkspaces } from '../../utils/filesystem/storage'; 8 7 import { ··· 20 19 import searchOverview from '../../experiments/search/icon.png'; 21 20 import customOverview from '../../experiments/custom/icon.png'; 22 21 23 - // conditions images 24 22 import multiConditionShape from '../../experiments/multitasking/stimuli/multiConditionShape.png'; 25 23 import multiConditionDots from '../../experiments/multitasking/stimuli/multiConditionDots.png'; 26 24 import conditionFace from '../../experiments/faces_houses/stimuli/faces/Face1.jpg'; ··· 70 68 this.handleStepClick = this.handleStepClick.bind(this); 71 69 this.handleStartExperiment = this.handleStartExperiment.bind(this); 72 70 this.handleCustomizeExperiment = this.handleCustomizeExperiment.bind(this); 73 - this.handleLoadCustomExperiment = 74 - this.handleLoadCustomExperiment.bind(this); 71 + this.handleLoadCustomExperiment = this.handleLoadCustomExperiment.bind(this); 75 72 this.handlePreview = this.handlePreview.bind(this); 76 73 this.endPreview = this.endPreview.bind(this); 77 74 this.handleEEGEnabled = this.handleEEGEnabled.bind(this); ··· 90 87 } 91 88 92 89 handleCustomizeExperiment() { 93 - this.setState({ 94 - isNewExperimentModalOpen: true, 95 - }); 90 + this.setState({ isNewExperimentModalOpen: true }); 96 91 } 97 92 98 93 handleLoadCustomExperiment(title: string) { 99 94 this.setState({ isNewExperimentModalOpen: false }); 100 - // Don't create new workspace if it already exists or title is too short 101 95 if (this.state.recentWorkspaces.includes(title)) { 102 96 toast.error(`Experiment already exists`); 103 97 return; ··· 131 125 132 126 static renderConditionIcon(condition) { 133 127 switch (condition) { 134 - case 'conditionCongruent': 135 - return conditionCongruent; 136 - case 'conditionIncongruent': 137 - return conditionIncongruent; 138 - case 'conditionOrangeT': 139 - return conditionOrangeT; 140 - case 'conditionNoOrangeT': 141 - return conditionNoOrangeT; 142 - case 'conditionFace': 143 - return conditionFace; 144 - case 'conditionHouse': 145 - return conditionHouse; 146 - case 'multiConditionShape': 147 - return multiConditionShape; 128 + case 'conditionCongruent': return conditionCongruent; 129 + case 'conditionIncongruent': return conditionIncongruent; 130 + case 'conditionOrangeT': return conditionOrangeT; 131 + case 'conditionNoOrangeT': return conditionNoOrangeT; 132 + case 'conditionFace': return conditionFace; 133 + case 'conditionHouse': return conditionHouse; 134 + case 'multiConditionShape': return multiConditionShape; 148 135 case 'multiConditionDots': 149 - default: 150 - return multiConditionDots; 136 + default: return multiConditionDots; 151 137 } 152 138 } 153 139 154 140 static renderOverviewIcon(type: EXPERIMENTS) { 155 141 switch (type) { 156 - case EXPERIMENTS.N170: 157 - return facesHousesOverview; 158 - case EXPERIMENTS.STROOP: 159 - return stroopOverview; 160 - case EXPERIMENTS.MULTI: 161 - return multitaskingOverview; 162 - case EXPERIMENTS.SEARCH: 163 - return searchOverview; 142 + case EXPERIMENTS.N170: return facesHousesOverview; 143 + case EXPERIMENTS.STROOP: return stroopOverview; 144 + case EXPERIMENTS.MULTI: return multitaskingOverview; 145 + case EXPERIMENTS.SEARCH: return searchOverview; 164 146 case EXPERIMENTS.CUSTOM: 165 - default: 166 - return customOverview; 147 + default: return customOverview; 167 148 } 168 149 } 169 150 ··· 176 157 case DESIGN_STEPS.OVERVIEW: 177 158 default: 178 159 return ( 179 - <div className={['flex items-center p-4', styles.contentGrid].join(' ')}> 160 + <div className="flex items-center p-4 h-[90%]"> 180 161 <div className="w-5/12 p-2"> 181 162 <img src={Design.renderOverviewIcon(this.props.type)} alt={overview.title} /> 182 163 </div> ··· 189 170 190 171 case DESIGN_STEPS.BACKGROUND: 191 172 return ( 192 - <div className={['flex items-center p-4', styles.contentGrid].join(' ')}> 173 + <div className="flex items-center p-4 h-[90%]"> 193 174 <div className="w-1/4 p-2"> 194 175 <img src={Design.renderOverviewIcon(this.props.type)} alt="overview" /> 195 176 </div> ··· 202 183 <p style={{ fontWeight: 'bold' }}>{background?.second_column_question}</p> 203 184 </div> 204 185 <div className="p-2"> 205 - <div className={styles.externalLinks}> 186 + <div className="grid grid-cols-1 gap-2.5"> 206 187 {background?.links.map((link) => ( 207 188 <Button 208 189 key={link.address} ··· 219 200 220 201 case DESIGN_STEPS.PROTOCOL: 221 202 return ( 222 - <div className={['flex items-center p-4', styles.contentGrid].join(' ')}> 203 + <div className="flex items-center p-4 h-[90%]"> 223 204 <div className="w-7/12 p-2 text-left"> 224 205 <h2>{protocol?.title}</h2> 225 206 <p>{protocol?.protocol}</p> ··· 253 234 254 235 case DESIGN_STEPS.PREVIEW: 255 236 return ( 256 - <div className={['flex items-center p-4', styles.contentGrid].join(' ')}> 257 - <div className={['w-3/4', styles.previewWindow].join(' ')}> 237 + <div className="flex items-center p-4 h-[90%]"> 238 + <div className="w-3/4 h-full border border-brand rounded"> 258 239 <PreviewExperimentComponent 259 240 title={this.props.title} 260 241 params={this.props.params} ··· 280 261 return <CustomDesign {...this.props} />; 281 262 } 282 263 return ( 283 - <div className={styles.mainContainer}> 264 + <div className="h-screen p-[3%] bg-gradient-to-b from-[#f9f9f9] to-[#f0f0ff]"> 284 265 <SecondaryNavComponent 285 266 title="Experiment Design" 286 267 steps={DESIGN_STEPS} ··· 291 272 type="checkbox" 292 273 defaultChecked={this.props.isEEGEnabled} 293 274 onChange={this.handleEEGEnabled} 294 - className={styles.EEGToggle} 275 + className="scale-75" 295 276 /> 296 277 } 297 278 />
+3 -7
src/renderer/components/EEGExplorationComponent.tsx
··· 11 11 import SignalQualityIndicatorComponent from './SignalQualityIndicatorComponent'; 12 12 import ViewerComponent from './ViewerComponent'; 13 13 import ConnectModal from './CollectComponent/ConnectModal'; 14 - import styles from './styles/common.module.css'; 15 14 import { DeviceActions } from '../actions'; 16 15 import { SignalQualityData } from '../constants/interfaces'; 17 16 ··· 51 50 52 51 handleStartConnect() { 53 52 this.setState({ isConnectModalOpen: true }); 54 - this.props.DeviceActions.SetDeviceAvailability( 55 - DEVICE_AVAILABILITY.SEARCHING 56 - ); 53 + this.props.DeviceActions.SetDeviceAvailability(DEVICE_AVAILABILITY.SEARCHING); 57 54 } 58 55 59 56 handleStopConnect() { ··· 68 65 69 66 render() { 70 67 return ( 71 - <div className={['flex items-center', styles.contentGrid].join(' ')}> 68 + <div className="flex items-center h-[90%]"> 72 69 {this.props.connectionStatus === CONNECTION_STATUS.CONNECTED && 73 70 this.props.signalQualityObservable && ( 74 71 <div className="flex w-full"> ··· 79 76 /> 80 77 </div> 81 78 <div className="w-3/5"> 82 - <div className={styles.disconnectButtonContainer}> 79 + <div className="flex justify-end"> 83 80 <Button variant="secondary" onClick={this.handleStopConnect}> 84 81 Disconnect EEG Device 85 82 </Button> ··· 97 94 <div className="w-5/12 p-2"> 98 95 <img src={eegImage} alt="EEG device" /> 99 96 </div> 100 - 101 97 <div className="w-7/12 p-2"> 102 98 <h1>Explore Raw EEG</h1> 103 99 <hr className="my-2" />
+17 -14
src/renderer/components/HomeComponent/ExperimentCard.tsx
··· 1 1 import React, { ReactElement } from 'react'; 2 - import styles from '../styles/common.module.css'; 2 + import { Card } from '../ui/card'; 3 3 4 4 interface ExperimentCardProps { 5 5 icon: any; ··· 15 15 description, 16 16 }: ExperimentCardProps): ReactElement { 17 17 return ( 18 - <div className="border border-gray-200 rounded-lg p-4 shadow-sm"> 19 - <div className={styles.experimentCard} onClick={onClick}> 20 - <div className="flex"> 21 - <div className={[styles.experimentCardImage, 'w-1/4'].join(' ')}> 22 - <img src={icon} alt={title} /> 23 - </div> 24 - <div className={[styles.descriptionContainer, 'w-3/4'].join(' ')}> 25 - <h1 className={styles.experimentCardHeader}>{title}</h1> 26 - <div className={styles.experimentCardDescription}> 27 - <p>{description}</p> 28 - </div> 29 - </div> 18 + <Card 19 + className="border-4 border-transparent hover:border-brand cursor-pointer transition-colors" 20 + onClick={onClick} 21 + > 22 + <div className="flex p-4 gap-4"> 23 + <div className="w-1/4 flex items-center"> 24 + <img src={icon} alt={title} /> 25 + </div> 26 + <div className="w-3/4 py-6"> 27 + <h1 className="text-[24px] tracking-[0.86px] leading-[29px] text-[#1a1a1a] font-normal mb-2"> 28 + {title} 29 + </h1> 30 + <p className="text-[16px] tracking-[0.57px] leading-[24px] text-[#4a4a4a]"> 31 + {description} 32 + </p> 30 33 </div> 31 34 </div> 32 - </div> 35 + </Card> 33 36 ); 34 37 }
+12 -16
src/renderer/components/HomeComponent/OverviewComponent.tsx
··· 1 - import React, { Component, useMemo, useState } from 'react'; 1 + import React, { useMemo, useState } from 'react'; 2 2 import { Button } from '../ui/button'; 3 - import styles from '../styles/common.module.css'; 4 3 import { EXPERIMENTS } from '../../constants/constants'; 5 4 import SecondaryNavComponent from '../SecondaryNavComponent'; 6 5 import { getExperimentFromType } from '../../utils/labjs/functions'; ··· 15 14 onCloseOverview: () => void; 16 15 } 17 16 18 - interface State { 19 - activeStep: OVERVIEW_STEPS; 17 + // Generic curried enum type guard 18 + function isEnum<T extends object>(en: T) { 19 + return (val: any): val is T[keyof T] => val in Object.values(en); 20 20 } 21 21 22 22 const OverviewComponent: React.FC<Props> = ({ ··· 31 31 setActiveStep(step); 32 32 } 33 33 }; 34 - const experiment = useMemo(() => { 35 - return getExperimentFromType(type); 36 - }, [type]); 34 + 35 + const experiment = useMemo(() => getExperimentFromType(type), [type]); 37 36 38 37 const renderSectionContent = () => { 39 38 switch (activeStep) { 40 39 case OVERVIEW_STEPS.OVERVIEW: 41 40 default: 42 41 return ( 43 - <div className={['flex items-center gap-8', styles.contentGrid].join(' ')}> 42 + <div className="flex items-center gap-8 h-[90%]"> 44 43 <div className="flex-1 text-right"> 45 44 <h1>{experiment?.text.overview.title}</h1> 46 45 </div> ··· 55 54 return ( 56 55 <> 57 56 <button 58 - className={styles.closeButton} 57 + className="flex justify-end w-full border-none shadow-none" 59 58 onClick={onCloseOverview} 60 59 aria-label="Close" 61 - >✕</button> 60 + > 61 + 62 + </button> 62 63 <SecondaryNavComponent 63 64 title={type} 64 65 steps={OVERVIEW_STEPS} ··· 70 71 </Button> 71 72 } 72 73 /> 73 - <div className={styles.homeContentContainer}> 74 + <div className="pt-5 h-full overflow-y-auto"> 74 75 {renderSectionContent()} 75 76 </div> 76 77 </> 77 78 ); 78 79 }; 79 - 80 - // Generic curreid enum type guard 81 - function isEnum<T extends object>(en: T) { 82 - return (val: any): val is T[keyof T] => val in Object.values(en); 83 - } 84 80 85 81 export default OverviewComponent;
+60 -94
src/renderer/components/HomeComponent/index.tsx
··· 1 1 import React, { Component } from 'react'; 2 2 import { isNil } from 'lodash'; 3 3 import { Button } from '../ui/button'; 4 - import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../ui/table'; 4 + import { Card } from '../ui/card'; 5 5 import { toast } from 'react-toastify'; 6 6 import dayjs from 'dayjs'; 7 7 import relativeTime from 'dayjs/plugin/relativeTime'; 8 8 dayjs.extend(relativeTime); 9 9 import { Observable } from 'rxjs'; 10 - import styles from '../styles/common.module.css'; 11 10 import { 12 11 EXPERIMENTS, 13 12 SCREENS, ··· 19 18 import stroopIcon from '../../experiments/stroop/icon.png'; 20 19 import multitaskingIcon from '../../experiments/multitasking/icon.png'; 21 20 import searchIcon from '../../experiments/search/icon.png'; 22 - // import customIcon from '../../experiments/custom/icon.png'; 23 21 import appLogo from '../../assets/common/app_logo.png'; 24 22 import divingMan from '../../assets/common/divingMan.svg'; 25 23 import { ··· 44 42 import PyodidePlotWidget from '../PyodidePlotWidget'; 45 43 46 44 const HOME_STEPS = { 47 - // TODO: maybe change the recent and new labels, but not necessary right now 48 45 RECENT: 'MY EXPERIMENTS', 49 46 NEW: 'EXPERIMENT BANK', 50 47 EXPLORE: 'EXPLORE EEG DATA', ··· 119 116 120 117 handleNewExperiment(experimentType: EXPERIMENTS) { 121 118 if (experimentType === EXPERIMENTS.CUSTOM) { 122 - this.setState({ 123 - isNewExperimentModalOpen: true, 124 - }); 125 - // If pre-designed experiment, load existing workspace 119 + this.setState({ isNewExperimentModalOpen: true }); 126 120 } else if (this.state.recentWorkspaces.includes(experimentType)) { 127 121 this.handleLoadRecentWorkspace(experimentType); 128 - // Create pre-designed workspace if opened for first time 129 122 } else { 130 123 this.props.ExperimentActions.CreateNewWorkspace({ 131 124 title: experimentType, ··· 137 130 138 131 handleLoadCustomExperiment(title: string) { 139 132 this.setState({ isNewExperimentModalOpen: false }); 140 - // Don't create new workspace if it already exists or title is too short 141 133 if (this.state.recentWorkspaces.includes(title)) { 142 134 toast.error(`Experiment already exists`); 143 135 return; ··· 153 145 this.props.navigate(SCREENS.DESIGN.route); 154 146 } 155 147 156 - // Load recent workspace by copying saved 'experiment' redux state into current redux state 157 148 async handleLoadRecentWorkspace(dir: string) { 158 149 const recentWorkspaceState = await readAndParseState(dir); 159 150 if (recentWorkspaceState == null) { ··· 162 153 ); 163 154 return; 164 155 } 165 - 166 - // This is a stop-gap solution until our lab.js experiment definitions for built-in experiments are fully serializable 167 - // Returns an appropriate default experiment object complete with initialization functions 168 156 const deserializedWorkspaceState = { 169 157 ...recentWorkspaceState, 170 158 experimentObject: getExperimentFromType(recentWorkspaceState.type) ··· 182 170 } 183 171 184 172 handleCloseOverview() { 185 - this.setState({ 186 - isOverviewComponentOpen: false, 187 - }); 173 + this.setState({ isOverviewComponentOpen: false }); 188 174 } 189 175 190 176 async handleDeleteWorkspace(dir) { ··· 201 187 } 202 188 } 203 189 204 - // TODO: Figure out how to make this not overflow when there's tons of workspaces. Lists? 205 190 renderSectionContent() { 206 191 switch (this.state.activeStep) { 207 192 case HOME_STEPS.RECENT: 208 193 return ( 209 - <div className={styles.myExperimentsPage}> 194 + <div className="pt-[50px]"> 210 195 {this.state.recentWorkspaces.length > 0 ? ( 211 - <Table> 212 - <TableHeader> 213 - <TableRow className={styles.experimentHeaderRow}> 214 - <TableHead className={styles.experimentHeaderName}> 215 - Experiment name 216 - </TableHead> 217 - <TableHead>Date Last Opened</TableHead> 218 - <TableHead className={styles.experimentHeaderActionsName}> 219 - Actions 220 - </TableHead> 221 - </TableRow> 222 - </TableHeader> 223 - 224 - <TableBody className={styles.experimentTable}> 225 - {this.state.recentWorkspaces 226 - .sort((a, b) => { 227 - const aState = this.state.workspaceStates[a]; 228 - const bState = this.state.workspaceStates[b]; 229 - 230 - const aTime = aState?.dateModified || 0; 231 - const bTime = bState?.dateModified || 0; 232 - 233 - return bTime - aTime; 234 - }) 235 - .map((dir) => { 236 - const workspaceState = this.state.workspaceStates[dir]; 237 - if (!workspaceState) { 238 - return undefined; 239 - } 240 - const { dateModified } = workspaceState; 241 - return ( 242 - <TableRow key={dir} className={styles.experimentRow}> 243 - <TableCell className={styles.experimentRowName}> 244 - {dir} 245 - </TableCell> 246 - <TableCell className={styles.experimentRowName}> 247 - {dateModified && dayjs(dateModified).fromNow()} 248 - </TableCell> 249 - <TableCell className={styles.experimentRowName}> 250 - <Button 251 - variant="secondary" 252 - onClick={() => this.handleDeleteWorkspace(dir)} 253 - > 254 - Delete 255 - </Button> 256 - <Button 257 - variant="secondary" 258 - onClick={() => openWorkspaceDir(dir)} 259 - > 260 - Go to Folder 261 - </Button> 262 - <Button 263 - variant="default" 264 - onClick={() => 265 - this.handleLoadRecentWorkspace(dir) 266 - } 267 - > 268 - Open Experiment 269 - </Button> 270 - </TableCell> 271 - </TableRow> 272 - ); 273 - })} 274 - </TableBody> 275 - </Table> 196 + <div className="space-y-2"> 197 + {/* Header row */} 198 + <div className="grid grid-cols-[1fr_1fr_auto] px-6 py-2 text-sm font-semibold text-[#666]"> 199 + <span>Experiment name</span> 200 + <span>Date Last Opened</span> 201 + <span className="min-w-[495px]">Actions</span> 202 + </div> 203 + {this.state.recentWorkspaces 204 + .sort((a, b) => { 205 + const aTime = this.state.workspaceStates[a]?.dateModified || 0; 206 + const bTime = this.state.workspaceStates[b]?.dateModified || 0; 207 + return bTime - aTime; 208 + }) 209 + .map((dir) => { 210 + const workspaceState = this.state.workspaceStates[dir]; 211 + if (!workspaceState) return undefined; 212 + const { dateModified } = workspaceState; 213 + return ( 214 + <Card 215 + key={dir} 216 + className="grid grid-cols-[1fr_1fr_auto] items-center px-6 py-3 rounded" 217 + > 218 + <span className="text-lg">{dir}</span> 219 + <span className="text-lg"> 220 + {dateModified && dayjs(dateModified).fromNow()} 221 + </span> 222 + <div className="flex gap-2"> 223 + <Button 224 + variant="secondary" 225 + onClick={() => this.handleDeleteWorkspace(dir)} 226 + > 227 + Delete 228 + </Button> 229 + <Button 230 + variant="secondary" 231 + onClick={() => openWorkspaceDir(dir)} 232 + > 233 + Go to Folder 234 + </Button> 235 + <Button 236 + variant="default" 237 + onClick={() => this.handleLoadRecentWorkspace(dir)} 238 + > 239 + Open Experiment 240 + </Button> 241 + </div> 242 + </Card> 243 + ); 244 + })} 245 + </div> 276 246 ) : ( 277 - <div className="text-center"> 278 - <img 279 - src={divingMan} 280 - className={styles.noExperimentsImage} 281 - alt="No experiments" 282 - /> 283 - <h2 className={styles.noExperimentsTitle}> 247 + <div className="text-center mt-[50px]"> 248 + <img src={divingMan} className="mx-auto" alt="No experiments" /> 249 + <h2 className="font-normal text-2xl leading-[29px] tracking-[-0.2px] text-[#1a1a1a] mt-4"> 284 250 You don&apos;t have any experiments yet 285 251 </h2> 286 - <p className={styles.noExperimentsText}> 252 + <p className="text-lg text-[#1a1a1a] tracking-[-0.2px]"> 287 253 Head over to the &quot;Experiment Bank&quot; section to start 288 254 an experiment. 289 255 </p> ··· 382 348 activeStep={this.state.activeStep} 383 349 onStepClick={this.handleStepClick} 384 350 /> 385 - <div className={styles.homeContentContainer}> 351 + <div className="pt-5 h-full overflow-y-auto"> 386 352 {this.renderSectionContent()} 387 353 </div> 388 354 </> ··· 391 357 392 358 render() { 393 359 return ( 394 - <div className={styles.mainContainer} data-tid="container"> 360 + <div className="h-screen p-[3%] bg-gradient-to-b from-[#f9f9f9] to-[#f0f0ff]" data-tid="container"> 395 361 {this.renderOverviewOrHome()} 396 362 <InputModal 397 363 open={this.state.isNewExperimentModalOpen}
+2 -3
src/renderer/components/InputCollect.tsx
··· 1 1 import React, { Component } from 'react'; 2 2 import { sanitizeTextInput } from '../utils/ui'; 3 - import styles from './styles/common.module.css'; 4 3 import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; 5 4 import { Button } from './ui/button'; 6 5 ··· 77 76 this.props.onExit(); 78 77 } 79 78 80 - handleEnterSubmit(event: KeyboardEvent) { 79 + handleEnterSubmit(event: React.KeyboardEvent<HTMLInputElement>) { 81 80 if (event.key === 'Enter') { 82 81 this.handleClose(); 83 82 } ··· 86 85 render() { 87 86 return ( 88 87 <Dialog open={this.props.open} onOpenChange={(open) => { if (!open) this.handleExit(); }}> 89 - <DialogContent className={styles.inputModal}> 88 + <DialogContent className="max-w-sm"> 90 89 <DialogHeader> 91 90 <DialogTitle>{this.props.header}</DialogTitle> 92 91 </DialogHeader>
+2 -3
src/renderer/components/InputModal.tsx
··· 1 1 import React, { Component } from 'react'; 2 2 import { debounce } from 'lodash'; 3 3 import { sanitizeTextInput } from '../utils/ui'; 4 - import styles from './styles/common.module.css'; 5 - import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; 4 + import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; 6 5 import { Button } from './ui/button'; 7 6 8 7 interface Props { ··· 55 54 render() { 56 55 return ( 57 56 <Dialog open={this.props.open} onOpenChange={(open) => { if (!open) this.handleExit(); }}> 58 - <DialogContent className={styles.inputModal}> 57 + <DialogContent className="max-w-sm text-center"> 59 58 <DialogHeader> 60 59 <DialogTitle>{this.props.header}</DialogTitle> 61 60 </DialogHeader>
+3 -5
src/renderer/components/PreviewExperimentComponent.tsx
··· 1 1 import React, { Component } from 'react'; 2 2 import { ExperimentWindow } from './ExperimentWindow'; 3 - import styles from './styles/collect.module.css'; 4 - 5 3 import { getImages } from '../utils/filesystem/storage'; 6 4 import { 7 5 ExperimentObject, ··· 30 28 render() { 31 29 if (!this.props.isPreviewing) { 32 30 return ( 33 - <div className={styles.previewPlaceholder}> 34 - <div className="p-2"> The experiment will be shown in the window </div> 31 + <div className="grid items-center justify-center h-full"> 32 + <div className="p-2">The experiment will be shown in the window</div> 35 33 </div> 36 34 ); 37 35 } 38 36 return ( 39 - <div className={styles.previewExpComponent}> 37 + <div className="h-full w-full flex"> 40 38 <ExperimentWindow 41 39 title={this.props.title} 42 40 experimentObject={this.props.experimentObject}
+8 -3
src/renderer/components/SecondaryNavComponent/SecondaryNavSegment.tsx
··· 1 1 import React from 'react'; 2 - import styles from '../styles/secondarynav.module.css'; 2 + import { cn } from '../ui/utils'; 3 3 4 4 interface Props { 5 5 title: string; 6 - style: string; 6 + active: boolean; 7 7 onClick: () => void; 8 8 } 9 9 ··· 11 11 return ( 12 12 <a 13 13 onClick={props.onClick} 14 - className={[props.style, styles.secondaryNavSegment, 'text-center flex items-end justify-center'].join(' ')} 14 + className={cn( 15 + 'flex items-end justify-center text-center text-sm font-bold tracking-[0.5px] border-b-4 min-w-fit px-4 pb-1 cursor-pointer', 16 + props.active 17 + ? 'text-[#1a1a1a] border-accent' 18 + : 'text-[#666] border-transparent hover:text-[#1a1a1a] hover:border-accent-light' 19 + )} 15 20 > 16 21 {props.title} 17 22 </a>
+42 -41
src/renderer/components/SecondaryNavComponent/index.tsx
··· 1 - import React, { Component, useState } from 'react'; 1 + import React, { Component } from 'react'; 2 2 import { NavLink } from 'react-router-dom'; 3 - import styles from '../styles/secondarynav.module.css'; 4 3 import SecondaryNavSegment from './SecondaryNavSegment'; 5 4 import { SCREENS } from '../../constants/constants'; 5 + import { 6 + DropdownMenu, 7 + DropdownMenuContent, 8 + DropdownMenuItem, 9 + DropdownMenuTrigger, 10 + } from '../ui/dropdown-menu'; 6 11 7 12 interface SettingsDropdownProps { 8 13 enableEEGToggle: JSX.Element; 9 14 saveButton?: JSX.Element; 10 - dropdownSettings: string; 11 - dropdownMenu: string; 12 - dropdownItem: string; 13 15 homeRoute: string; 14 16 } 15 17 16 - function SettingsDropdown({ enableEEGToggle, saveButton, dropdownSettings, dropdownMenu, dropdownItem, homeRoute }: SettingsDropdownProps) { 17 - const [open, setOpen] = useState(false); 18 + function SettingsDropdown({ enableEEGToggle, saveButton, homeRoute }: SettingsDropdownProps) { 18 19 return ( 19 - <div style={{ position: 'relative', display: 'inline-block' }}> 20 - <button className={dropdownSettings} onClick={() => setOpen((o) => !o)} aria-label="Settings"> 21 - 22 - </button> 23 - {open && ( 24 - <div className={dropdownMenu} style={{ position: 'absolute', right: 0, zIndex: 50 }}> 25 - <div className={dropdownItem} onClick={(e) => e.stopPropagation()}> 26 - <div>Enable EEG</div> 20 + <div className="flex items-center gap-2 pr-4"> 21 + {saveButton} 22 + <DropdownMenu> 23 + <DropdownMenuTrigger className="text-2xl text-[#666] focus:outline-none px-2"> 24 + 25 + </DropdownMenuTrigger> 26 + <DropdownMenuContent align="end" className="min-w-[240px]"> 27 + <DropdownMenuItem 28 + onSelect={(e) => e.preventDefault()} 29 + className="flex items-center justify-between" 30 + > 31 + <span>Enable EEG</span> 27 32 {enableEEGToggle} 28 - </div> 29 - <div className={dropdownItem}> 30 - <NavLink to={homeRoute} onClick={() => setOpen(false)}> 31 - <p>Exit Experiment</p> 33 + </DropdownMenuItem> 34 + <DropdownMenuItem asChild> 35 + <NavLink to={homeRoute} className="w-full"> 36 + Exit Experiment 32 37 </NavLink> 33 - </div> 34 - </div> 35 - )} 36 - {saveButton} 38 + </DropdownMenuItem> 39 + </DropdownMenuContent> 40 + </DropdownMenu> 37 41 </div> 38 42 ); 39 43 } ··· 57 61 renderTitle() { 58 62 if (typeof this.props.title === 'string') { 59 63 return ( 60 - <span className={styles.secondaryNavContainerExpName}> 64 + <span className="font-normal text-2xl leading-[29px] tracking-[-0.2px] text-[#1a1a1a]"> 61 65 {this.props.title} 62 66 </span> 63 67 ); ··· 72 76 <SecondaryNavSegment 73 77 key={stepTitle} 74 78 title={stepTitle} 75 - style={ 76 - this.props.activeStep === stepTitle 77 - ? styles.activeSecondaryNavSegment 78 - : styles.inactiveSecondaryNavSegment 79 - } 79 + active={this.props.activeStep === stepTitle} 80 80 onClick={() => this.props.onStepClick(stepTitle)} 81 81 /> 82 82 ))} ··· 86 86 87 87 render() { 88 88 return ( 89 - <div className={['flex items-center', styles.secondaryNavContainer].join(' ')}> 90 - <div className="w-1/4 flex items-end"> 89 + <div className="flex items-center"> 90 + <div className="w-1/4 flex items-end px-4 py-2"> 91 91 {this.renderTitle()} 92 92 </div> 93 93 ··· 95 95 96 96 {this.props.enableEEGToggle && ( 97 97 <div className="ml-auto"> 98 - <div className={styles.settingsButtons}> 99 - <SettingsDropdown 100 - enableEEGToggle={this.props.enableEEGToggle} 101 - saveButton={this.props.saveButton} 102 - dropdownSettings={styles.dropdownSettings} 103 - dropdownMenu={styles.dropdownMenu} 104 - dropdownItem={styles.dropdownItem} 105 - homeRoute={SCREENS.HOME.route} 106 - /> 107 - </div> 98 + <SettingsDropdown 99 + enableEEGToggle={this.props.enableEEGToggle} 100 + saveButton={this.props.saveButton} 101 + homeRoute={SCREENS.HOME.route} 102 + /> 103 + </div> 104 + )} 105 + 106 + {!this.props.enableEEGToggle && this.props.saveButton && ( 107 + <div className="ml-auto pr-4"> 108 + {this.props.saveButton} 108 109 </div> 109 110 )} 110 111 </div>
+27 -5
src/renderer/components/TopNavComponent/PrimaryNavSegment.tsx
··· 1 1 import React from 'react'; 2 2 import { NavLink } from 'react-router-dom'; 3 - import styles from '../styles/topnavbar.module.css'; 3 + import { cn } from '../ui/utils'; 4 4 5 5 interface Props { 6 - style: string; 6 + status: 'active' | 'visited' | 'initial'; 7 7 route: string; 8 8 title: string; 9 9 order: number; 10 10 } 11 11 12 + const navColumnBase = 13 + 'flex justify-center items-center h-full text-sm font-bold tracking-[0.5px] border-b-4 px-4'; 14 + 15 + const statusStyles = { 16 + active: 'text-[#1a1a1a] border-accent', 17 + visited: 'text-[#1a1a1a] border-accent', 18 + initial: 'text-[#666] border-transparent hover:text-[#1a1a1a] hover:border-accent-light', 19 + }; 20 + 21 + const bubbleStyles = { 22 + active: 'border-accent text-accent', 23 + visited: 'border-[#666] text-[#666]', 24 + initial: 'border-[#ccc] text-[#ccc]', 25 + }; 26 + 12 27 const PrimaryNavSegment = (props: Props) => { 13 28 return ( 14 - <div className={[props.style, styles.navColumn].join(' ')}> 15 - <NavLink to={props.route}> 16 - <div className={styles.numberBubble}>{props.order}</div> 29 + <div className={cn(navColumnBase, statusStyles[props.status])}> 30 + <NavLink to={props.route} className="flex items-center gap-2"> 31 + <span 32 + className={cn( 33 + 'inline-flex items-center justify-center rounded-full h-[23px] w-[23px] border-2 text-xs', 34 + bubbleStyles[props.status] 35 + )} 36 + > 37 + {props.order} 38 + </span> 17 39 {props.title} 18 40 </NavLink> 19 41 </div>
+37 -39
src/renderer/components/TopNavComponent/index.tsx
··· 2 2 import { NavLink, useLocation } from 'react-router-dom'; 3 3 import { isNil } from 'lodash'; 4 4 import { EXPERIMENTS, SCREENS } from '../../constants/constants'; 5 - import styles from '../styles/topnavbar.module.css'; 6 5 import PrimaryNavSegment from './PrimaryNavSegment'; 7 6 import { 8 7 readAndParseState, ··· 10 9 } from '../../utils/filesystem/storage'; 11 10 import BrainwavesIcon from '../../assets/common/Brainwaves_Icon_big.png'; 12 11 import { ExperimentActions } from '../../actions'; 12 + import { 13 + DropdownMenu, 14 + DropdownMenuContent, 15 + DropdownMenuItem, 16 + DropdownMenuTrigger, 17 + } from '../ui/dropdown-menu'; 13 18 14 19 export interface Props { 15 20 title: string | null | undefined; ··· 23 28 const location = useLocation(); 24 29 const [recentWorkspaces, setRecentWorkspaces] = useState<string[]>([]); 25 30 26 - const getStyleForScreen = ( 31 + const getStatusForScreen = ( 27 32 navSegmentScreen: (typeof SCREENS)[keyof typeof SCREENS] 28 - ) => { 33 + ): 'active' | 'visited' | 'initial' => { 29 34 if (navSegmentScreen.route === location.pathname) { 30 - return styles.activeNavColumn; 35 + return 'active'; 31 36 } 32 37 const routeOrder = Object.values(SCREENS).find( 33 38 (screen) => screen.route === navSegmentScreen.route ··· 36 41 (screen) => screen.route === location.pathname 37 42 )?.order; 38 43 if (routeOrder && currentOrder && currentOrder > routeOrder) { 39 - return styles.visitedNavColumn; 44 + return 'visited'; 40 45 } 41 - return styles.initialNavColumn; 46 + return 'initial'; 42 47 }; 43 48 44 49 const updateWorkspaces = async () => { ··· 62 67 } 63 68 64 69 return ( 65 - <div className={styles.navContainer}> 66 - <div className={styles.experimentTitleGridColumn}> 67 - <div className={styles.homeButton}> 68 - <NavLink to={SCREENS.HOME.route}> 69 - <img 70 - className={styles.exitWorkspaceBtn} 71 - src={BrainwavesIcon} 72 - alt="Home" 73 - /> 70 + <div className="relative z-[999] h-[60px] bg-white shadow-[0_5px_16px_0_rgba(0,0,0,0.09)] flex items-center"> 71 + {/* Home button */} 72 + <div className="flex justify-center items-center h-full border-b-4 border-accent px-4"> 73 + <div className="flex justify-center ml-5"> 74 + <NavLink to={SCREENS.HOME.route} className="flex items-center gap-1 text-sm"> 75 + <img src={BrainwavesIcon} alt="Home" className="h-6 w-auto" /> 74 76 Home 75 77 </NavLink> 76 78 </div> 77 79 </div> 78 80 79 - <div className={styles.experimentTitleGridColumn}> 80 - <div className="relative"> 81 - <button 82 - onClick={updateWorkspaces} 83 - className={styles.workspaceDropdownTrigger} 84 - > 85 - {props.title ? props.title : 'Untitled'} 86 - </button> 81 + {/* Workspace title / recent workspaces dropdown */} 82 + <div className="flex justify-center items-center h-full text-lg tracking-[0.5px] border-b-4 border-accent px-4"> 83 + <DropdownMenu onOpenChange={(open) => { if (open) updateWorkspaces(); }}> 84 + <DropdownMenuTrigger className="focus:outline-none font-medium"> 85 + {props.title ? props.title : 'Untitled'} ▾ 86 + </DropdownMenuTrigger> 87 87 {recentWorkspaces.length > 0 && ( 88 - <ul className={styles.workspaceDropdownMenu}> 88 + <DropdownMenuContent align="start"> 89 89 {recentWorkspaces.map((workspace) => ( 90 - <li key={workspace}> 91 - <button 92 - onClick={() => handleLoadRecentWorkspace(workspace)} 93 - className={styles.workspaceDropdownItem} 94 - > 95 - {workspace} 96 - </button> 97 - </li> 90 + <DropdownMenuItem 91 + key={workspace} 92 + onClick={() => handleLoadRecentWorkspace(workspace)} 93 + > 94 + {workspace} 95 + </DropdownMenuItem> 98 96 ))} 99 - </ul> 97 + </DropdownMenuContent> 100 98 )} 101 - </div> 99 + </DropdownMenu> 102 100 </div> 103 101 104 102 <PrimaryNavSegment 105 103 {...SCREENS.DESIGN} 106 - style={getStyleForScreen(SCREENS.DESIGN)} 104 + status={getStatusForScreen(SCREENS.DESIGN)} 107 105 /> 108 106 <PrimaryNavSegment 109 107 {...SCREENS.COLLECT} 110 - style={getStyleForScreen(SCREENS.COLLECT)} 108 + status={getStatusForScreen(SCREENS.COLLECT)} 111 109 /> 112 110 {props.isEEGEnabled ? ( 113 111 <PrimaryNavSegment 114 112 {...SCREENS.CLEAN} 115 - style={getStyleForScreen(SCREENS.CLEAN)} 113 + status={getStatusForScreen(SCREENS.CLEAN)} 116 114 /> 117 115 ) : null} 118 116 {props.isEEGEnabled ? ( 119 117 <PrimaryNavSegment 120 118 {...SCREENS.ANALYZE} 121 - style={getStyleForScreen(SCREENS.ANALYZE)} 119 + status={getStatusForScreen(SCREENS.ANALYZE)} 122 120 /> 123 121 ) : ( 124 122 <PrimaryNavSegment 125 123 {...SCREENS.ANALYZEBEHAVIOR} 126 - style={getStyleForScreen(SCREENS.ANALYZE)} 124 + status={getStatusForScreen(SCREENS.ANALYZE)} 127 125 /> 128 126 )} 129 127 </div>
-101
src/renderer/components/styles/collect.module.css
··· 1 - .connectModal { 2 - color: #fff; 3 - font-size: 18px !important; 4 - width: 25% !important; 5 - text-align: center; 6 - } 7 - 8 - .connectModal a { 9 - text-decoration: underline; 10 - margin-left: 0; 11 - } 12 - 13 - .connectHeader { 14 - font-size: 36px !important; 15 - letter-spacing: 1.29px !important; 16 - line-height: 44px !important; 17 - font-weight: normal !important; 18 - } 19 - 20 - .modalCloseButton { 21 - position: absolute; 22 - top: -40px; 23 - left: -40px; 24 - } 25 - 26 - .searchingText { 27 - text-align: center !important; 28 - } 29 - 30 - .deviceItem { 31 - font-size: 18px !important; 32 - border-top: none !important; 33 - } 34 - 35 - .preTestContainer { 36 - padding: 3% !important; 37 - } 38 - 39 - .preTestPushable { 40 - height: 100vh !important; 41 - background: linear-gradient(#f9f9f9, #f0f0ff) !important; 42 - } 43 - 44 - .previewEEGWindow { 45 - height: 100%; 46 - align-items: center; 47 - margin-bottom: 20px; 48 - } 49 - 50 - .previewPlaceholder { 51 - display: grid; 52 - align-items: center; 53 - justify-content: center; 54 - } 55 - 56 - .previewExpComponent { 57 - height: 100%; 58 - width: 100%; 59 - display: flex; 60 - } 61 - 62 - .primaryButton { 63 - font-size: 18px !important; 64 - font-weight: normal !important; 65 - color: #007c70 !important; 66 - border: solid 2px #fff !important; 67 - border-radius: 4px !important; 68 - background-color: #fff !important; 69 - } 70 - 71 - .primaryButton:hover { 72 - background-color: #e6f2f1 !important; 73 - } 74 - 75 - .secondaryButton { 76 - font-size: 18px !important; 77 - font-weight: normal !important; 78 - color: #fff !important; 79 - border: solid 2px #fff !important; 80 - border-radius: 4px !important; 81 - background-color: rgba(255, 255, 255, 0) !important; 82 - } 83 - 84 - .secondaryButton:hover { 85 - background-color: #00635a !important; 86 - } 87 - 88 - .greatSignal { 89 - color: #66b0a9; 90 - } 91 - .okSignal { 92 - color: #ffcd39; 93 - } 94 - 95 - .badSignal { 96 - color: #e06766; 97 - } 98 - 99 - .noSignal { 100 - color: #bfbfbf; 101 - }
-381
src/renderer/components/styles/common.module.css
··· 1 - .homeContentContainer { 2 - padding-top: 20px !important; 3 - height: 100%; 4 - overflow-y: auto; 5 - } 6 - 7 - .mainContainer { 8 - padding: 3%; 9 - height: 100vh; 10 - background: linear-gradient(#f9f9f9, #f0f0ff); 11 - } 12 - 13 - .mainContainer h2 { 14 - font-size: 5rem; 15 - } 16 - 17 - .mainContainer a { 18 - font-size: 1.4rem; 19 - } 20 - 21 - .mainSegment { 22 - width: 100%; 23 - } 24 - 25 - .contentGrid { 26 - height: 90%; 27 - } 28 - 29 - .overviewImage { 30 - height: 140px !important; 31 - } 32 - 33 - .inputDiv { 34 - margin: 15px; 35 - } 36 - 37 - .label { 38 - font-size: 14px !important; 39 - font-weight: bold; 40 - line-height: 17px; 41 - } 42 - 43 - .appLogo { 44 - height: auto; 45 - width: 250px; 46 - background-color: #007c70 !important; 47 - } 48 - 49 - .inputModal { 50 - font-size: 18px !important; 51 - width: 25% !important; 52 - text-align: center !important; 53 - } 54 - 55 - .descriptionContainer p { 56 - margin-top: 50px; 57 - margin-bottom: 50px; 58 - min-height: 130px; 59 - } 60 - 61 - .descriptionContainer button { 62 - margin-right: 20px !important; 63 - } 64 - 65 - .infoSegment { 66 - padding: 0% !important; 67 - font-size: 18px !important; 68 - } 69 - 70 - .plotSegment { 71 - padding: 0% !important; 72 - font-size: 18px !important; 73 - } 74 - 75 - .closeButton { 76 - border: none !important; 77 - box-shadow: none !important; 78 - display: grid; 79 - justify-content: end; 80 - } 81 - 82 - .previewWindow { 83 - height: inherit; 84 - align-items: center; 85 - margin-bottom: 20px; 86 - border: solid 1px #007c70; 87 - border-radius: 4px; 88 - } 89 - 90 - .experimentWindow { 91 - height: 100%; 92 - width: 100%; 93 - } 94 - 95 - .paramSlider { 96 - width: 200px; 97 - } 98 - 99 - .recentDirSegment { 100 - margin-left: 20px !important; 101 - } 102 - 103 - .experimentContainer { 104 - height: 100%; 105 - } 106 - 107 - .analyzeColumn { 108 - height: 100%; 109 - } 110 - 111 - .helpButton { 112 - height: 44px !important; 113 - width: 44px !important; 114 - color: #fff !important; 115 - background-color: #007c70 !important; 116 - border-radius: 22px !important; 117 - } 118 - 119 - .helpSidebar { 120 - height: inherit; 121 - } 122 - 123 - .helpHeader { 124 - margin-bottom: 18px !important; 125 - } 126 - 127 - .helpMenuItem { 128 - font-size: 18px !important; 129 - padding: 5px !important; 130 - } 131 - 132 - .helpContent { 133 - font-size: 18px !important; 134 - height: 80%; 135 - } 136 - 137 - .externalLinks { 138 - display: grid; 139 - grid-template-columns: 1fr; 140 - grid-row-gap: 10px; 141 - } 142 - 143 - .experimentCard { 144 - border-radius: 5px; 145 - background-color: #fff; 146 - box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1); 147 - border: 4px solid transparent; 148 - } 149 - 150 - .experimentCard:hover { 151 - border: 4px solid #007c70; 152 - cursor: pointer; 153 - } 154 - 155 - .experimentCardImage { 156 - align-self: center; 157 - } 158 - 159 - /* Header, Faces/Houses, Stroop, Mult-tasking, Visual Search */ 160 - .experimentCardHeader { 161 - color: #1a1a1a; 162 - font-family: Lato, sans-serif; 163 - font-size: 24px; 164 - letter-spacing: 0.86px; 165 - line-height: 29px; 166 - } 167 - 168 - /* Description copy */ 169 - .experimentCardDescription { 170 - color: #4a4a4a; 171 - font-family: Lato, sans-serif; 172 - font-size: 16px; 173 - letter-spacing: 0.57px; 174 - line-height: 24px; 175 - } 176 - 177 - .noExperimentsImage { 178 - margin-top: 50px; 179 - } 180 - 181 - .noExperimentsTitle { 182 - font-family: Lato, sans-serif; 183 - font-style: normal; 184 - font-weight: normal !important; 185 - font-size: 24px !important; 186 - line-height: 29px; 187 - letter-spacing: -0.2px; 188 - color: #1a1a1a; 189 - } 190 - 191 - .noExperimentsText { 192 - font-family: Lato, sans-serif; 193 - font-style: normal; 194 - font-weight: normal; 195 - font-size: 18px; 196 - line-height: 24px; 197 - letter-spacing: -0.2px; 198 - color: #1a1a1a; 199 - } 200 - 201 - .myExperimentsPage { 202 - padding-top: 50px !important; 203 - } 204 - 205 - .experimentRow { 206 - background-color: #fff !important; 207 - box-shadow: 0 0 8px rgba(0, 0, 0, 0.1); 208 - border-radius: 4px; 209 - display: grid; 210 - grid-template-columns: 1fr 1fr auto; 211 - align-items: center; 212 - justify-content: left; 213 - } 214 - 215 - .experimentRowName { 216 - font-family: Lato, sans-serif; 217 - font-style: normal !important; 218 - font-weight: normal !important; 219 - font-size: 18px !important; 220 - line-height: 24px !important; 221 - letter-spacing: -0.2px; 222 - padding-left: 26px !important; 223 - border: 0 !important; 224 - padding-right: 10px !important; 225 - } 226 - 227 - .experimentHeaderName { 228 - padding-left: 26px !important; 229 - } 230 - 231 - .experimentHeaderRow { 232 - display: grid; 233 - grid-template-columns: 1fr 1fr auto; 234 - } 235 - 236 - .experimentTable { 237 - display: grid; 238 - grid-row-gap: 10px; 239 - align-items: center; 240 - grid-template-columns: 1fr; 241 - } 242 - 243 - .experimentHeaderActionsName { 244 - min-width: 495px !important; 245 - } 246 - 247 - .conditionsNameRow { 248 - font-family: Lato, sans-serif; 249 - font-style: normal !important; 250 - font-weight: normal !important; 251 - font-size: 18px !important; 252 - line-height: 24px !important; 253 - letter-spacing: -0.2px; 254 - color: #1a1a1a; 255 - padding-left: 26px !important; 256 - border: 0 !important; 257 - padding-right: 10px !important; 258 - display: grid; 259 - grid-template-columns: 50px 1fr; 260 - align-items: center; 261 - } 262 - 263 - .conditionHeaderRowName { 264 - padding-left: 60px !important; 265 - } 266 - 267 - .conditionRow { 268 - background-color: #fff !important; 269 - box-shadow: 0 0 8px rgba(0, 0, 0, 0.1); 270 - border-radius: 4px; 271 - display: grid; 272 - grid-template-columns: 300px 200px 1fr; 273 - align-items: center; 274 - justify-content: left; 275 - } 276 - 277 - .conditionHeaderRow { 278 - display: grid; 279 - grid-template-columns: 300px 200px 1fr; 280 - padding-left: 20px !important; 281 - } 282 - 283 - .trialsHeader { 284 - display: grid; 285 - grid-template-columns: auto 1fr; 286 - width: 100%; 287 - justify-items: end; 288 - } 289 - 290 - .trialsHeaderRow { 291 - display: grid; 292 - grid-template-columns: 1fr 1fr 180px 350px; 293 - padding-left: 20px !important; 294 - } 295 - 296 - .trialsRow { 297 - background-color: #fff !important; 298 - box-shadow: 0 0 8px rgba(0, 0, 0, 0.1); 299 - border-radius: 4px; 300 - display: grid; 301 - grid-template-columns: 1fr 1fr 180px 350px; 302 - align-items: center; 303 - justify-content: left; 304 - } 305 - 306 - .trialsTrialTypeRow { 307 - font-family: Lato, sans-serif; 308 - font-style: normal !important; 309 - font-weight: normal !important; 310 - font-size: 18px !important; 311 - line-height: 24px !important; 312 - letter-spacing: -0.2px; 313 - color: #1a1a1a; 314 - padding-left: 26px !important; 315 - border: 0 !important; 316 - padding-right: 10px !important; 317 - display: grid; 318 - grid-template-columns: 1fr 120px; 319 - align-items: center; 320 - grid-column-gap: 30px; 321 - } 322 - 323 - .trialsTrialTypeSegment { 324 - display: grid; 325 - grid-template-columns: auto 1fr; 326 - align-items: center; 327 - margin: 0 !important; 328 - padding: 10px 12px 10px 12px !important; 329 - border: 2px solid #ccc !important; 330 - border-radius: 4px !important; 331 - } 332 - 333 - .trialsTrialTypeRowSelector { 334 - /* background-color: #EB1B66 !important; */ 335 - color: white !important; 336 - 337 - /* background-color: `${this.props.phase === 'main' ? '#1AC4EF' : '#EB1B66'}; */ 338 - font-family: Lato, sans-serif !important; 339 - font-style: normal !important; 340 - font-weight: bold !important; 341 - font-size: 14px !important; 342 - line-height: 24px !important; 343 - letter-spacing: -0.2px !important; 344 - border-radius: 27px; 345 - padding: 0 7px 0 7px !important; 346 - } 347 - 348 - .trialsTable { 349 - display: grid; 350 - grid-row-gap: 10px; 351 - align-items: center; 352 - align-content: baseline; 353 - grid-template-columns: 1fr; 354 - overflow-y: scroll; 355 - max-height: 50vh; 356 - } 357 - 358 - .EEGToggle { 359 - transform: scale(0.7); 360 - } 361 - 362 - .selectedFolderContainer { 363 - display: grid; 364 - grid-template-columns: auto auto 1fr; 365 - grid-column-gap: 10px; 366 - border: 2px solid #ccc; 367 - padding: inherit; 368 - border-radius: 4px; 369 - width: fit-content; 370 - } 371 - 372 - .disconnectButtonContainer { 373 - display: flex; 374 - justify-content: flex-end; 375 - } 376 - 377 - .trialsTopInfoBar { 378 - display: grid; 379 - grid-template-columns: 1fr 1fr 1fr; 380 - grid-column-gap: 10px; 381 - }
-68
src/renderer/components/styles/secondarynav.module.css
··· 1 - /* Secondary Nav related styles */ 2 - 3 - .secondaryNavContainer { 4 - margin-bottom: 0 !important; 5 - } 6 - 7 - .secondaryNavSegment { 8 - display: flex !important; 9 - justify-content: center !important; 10 - border-style: solid; 11 - border-width: 0; 12 - border-bottom-width: 4px; 13 - font-size: 14px !important; 14 - font-weight: bold; 15 - line-height: 17px; 16 - letter-spacing: 0.5px; 17 - min-width: fit-content; 18 - } 19 - 20 - .inactiveSecondaryNavSegment { 21 - color: #666; 22 - border-color: #666; 23 - border-bottom-color: rgba(255, 255, 255, 0); 24 - } 25 - 26 - .activeSecondaryNavSegment { 27 - color: #1a1a1a; 28 - border-bottom-color: #ffc107; 29 - } 30 - 31 - .inactiveSecondaryNavSegment:hover { 32 - color: #1a1a1a; 33 - border-bottom-color: #ffe69c; 34 - } 35 - 36 - .secondaryNavContainerExpName { 37 - font-family: Lato, sans-serif; 38 - font-style: normal; 39 - font-weight: normal !important; 40 - font-size: 24px !important; 41 - line-height: 29px !important; 42 - letter-spacing: -0.2px !important; 43 - color: #1a1a1a !important; 44 - } 45 - 46 - .settingsButtons { 47 - display: grid; 48 - grid-template-columns: 2fr 4fr; 49 - } 50 - 51 - .dropdownSettings { 52 - font-size: 27px; 53 - color: #666; 54 - display: grid !important; 55 - align-items: center; 56 - padding-right: 20px; 57 - } 58 - 59 - .dropdownMenu { 60 - margin-left: 30px !important; 61 - } 62 - 63 - .dropdownItem { 64 - display: grid !important; 65 - grid-template-columns: 3fr 1fr !important; 66 - min-width: 240px !important; 67 - align-items: center; 68 - }
-114
src/renderer/components/styles/topnavbar.module.css
··· 1 - .navContainer { 2 - margin-top: 0 !important; 3 - position: relative; 4 - z-index: 999; 5 - height: 60px; 6 - background: #fff; 7 - box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.09); 8 - } 9 - 10 - .statusHeader { 11 - color: #484848; 12 - } 13 - 14 - .navColumn { 15 - display: flex !important; 16 - justify-content: center; 17 - height: inherit; 18 - font-size: 14px; 19 - font-weight: bold; 20 - line-height: 17px; 21 - letter-spacing: 0.5px; 22 - border-style: solid; 23 - border-width: 0; 24 - border-bottom-width: 4px; 25 - } 26 - 27 - .initialNavColumn { 28 - color: #666; 29 - border-color: #666; 30 - border-bottom-color: #fff; 31 - } 32 - 33 - .visitedNavColumn { 34 - color: #1a1a1a; 35 - border-style: solid; 36 - border-bottom-color: #ffc107; 37 - } 38 - 39 - .activeNavColumn { 40 - color: #1a1a1a; 41 - border-bottom-color: #ffc107; 42 - } 43 - 44 - .initialNavColumn:hover { 45 - color: #1a1a1a; 46 - border-bottom-color: #ffe69c; 47 - } 48 - 49 - .numberBubble { 50 - font-size: 14px; 51 - display: inline-block; 52 - border-radius: 50%; 53 - box-sizing: border-box; 54 - height: 23px; 55 - width: 23px; 56 - margin-right: 10px; 57 - text-align: center; 58 - } 59 - 60 - .initialNavColumn .numberBubble { 61 - color: #ccc; 62 - border: 2px solid #ccc; 63 - } 64 - 65 - .visitedNavColumn .numberBubble { 66 - color: #666; 67 - border: 2px solid #666; 68 - } 69 - 70 - .activeNavColumn .numberBubble { 71 - color: #ffc107; 72 - border: 2px solid #ffc107; 73 - } 74 - 75 - .closeButton { 76 - border: none !important; 77 - box-shadow: none !important; 78 - } 79 - 80 - .experimentTitleGridColumn { 81 - /* white-space: nowrap; */ 82 - display: flex !important; 83 - justify-content: center; 84 - height: inherit; 85 - font-size: 18px; 86 - letter-spacing: 0.5px; 87 - line-height: 17px; 88 - color: #1a1a1a; 89 - border-style: solid; 90 - border-width: 0; 91 - border-bottom-width: 4px; 92 - border-bottom-color: #ffc107; 93 - width: inherit; 94 - } 95 - 96 - .homeButton { 97 - display: grid; 98 - justify-content: center; 99 - margin-left: 20px !important; 100 - } 101 - 102 - .exitWorkspaceBtn { 103 - font-family: 104 - Gothic A1, 105 - sans-serif !important; 106 - font-style: normal !important; 107 - font-weight: 900 !important; 108 - font-size: 24px !important; 109 - line-height: 30px !important; 110 - color: #007c70 !important; 111 - border: none !important; 112 - height: 30px; 113 - min-width: 30px; 114 - }
+31
src/renderer/components/ui/badge.tsx
··· 1 + import * as React from 'react'; 2 + import { cva, type VariantProps } from 'class-variance-authority'; 3 + import { cn } from './utils'; 4 + 5 + const badgeVariants = cva( 6 + 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors', 7 + { 8 + variants: { 9 + variant: { 10 + default: 'bg-brand text-white', 11 + secondary: 'bg-gray-100 text-gray-900', 12 + outline: 'border border-gray-300 text-gray-900', 13 + experimental: 'bg-[#1AC4EF] text-white', 14 + practice: 'bg-[#EB1B66] text-white', 15 + }, 16 + }, 17 + defaultVariants: { 18 + variant: 'default', 19 + }, 20 + } 21 + ); 22 + 23 + export interface BadgeProps 24 + extends React.HTMLAttributes<HTMLDivElement>, 25 + VariantProps<typeof badgeVariants> {} 26 + 27 + function Badge({ className, variant, ...props }: BadgeProps) { 28 + return <div className={cn(badgeVariants({ variant }), className)} {...props} />; 29 + } 30 + 31 + export { Badge, badgeVariants };
+3 -3
src/renderer/components/ui/button.tsx
··· 8 8 { 9 9 variants: { 10 10 variant: { 11 - default: 'bg-blue-600 text-white hover:bg-blue-700', 12 - secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300', 11 + default: 'bg-brand text-white hover:bg-brand-dark', 12 + secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200', 13 13 outline: 14 14 'border border-gray-300 bg-transparent hover:bg-gray-100 text-gray-900', 15 15 ghost: 'hover:bg-gray-100 text-gray-900', 16 16 destructive: 'bg-red-600 text-white hover:bg-red-700', 17 - link: 'text-blue-600 underline-offset-4 hover:underline', 17 + link: 'text-brand underline-offset-4 hover:underline', 18 18 }, 19 19 size: { 20 20 default: 'h-9 px-4 py-2',
+100
src/renderer/components/ui/select.tsx
··· 1 + import * as React from 'react'; 2 + import * as SelectPrimitive from '@radix-ui/react-select'; 3 + import { cn } from './utils'; 4 + 5 + const Select = SelectPrimitive.Root; 6 + const SelectGroup = SelectPrimitive.Group; 7 + const SelectValue = SelectPrimitive.Value; 8 + 9 + const SelectTrigger = React.forwardRef< 10 + React.ElementRef<typeof SelectPrimitive.Trigger>, 11 + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> 12 + >(({ className, children, ...props }, ref) => ( 13 + <SelectPrimitive.Trigger 14 + ref={ref} 15 + className={cn( 16 + 'flex h-9 w-full items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-brand disabled:cursor-not-allowed disabled:opacity-50', 17 + className 18 + )} 19 + {...props} 20 + > 21 + {children} 22 + <SelectPrimitive.Icon asChild> 23 + <span className="ml-2 text-gray-400">▾</span> 24 + </SelectPrimitive.Icon> 25 + </SelectPrimitive.Trigger> 26 + )); 27 + SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 28 + 29 + const SelectContent = React.forwardRef< 30 + React.ElementRef<typeof SelectPrimitive.Content>, 31 + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> 32 + >(({ className, children, position = 'popper', ...props }, ref) => ( 33 + <SelectPrimitive.Portal> 34 + <SelectPrimitive.Content 35 + ref={ref} 36 + className={cn( 37 + 'relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white shadow-md', 38 + position === 'popper' && 'translate-y-1', 39 + className 40 + )} 41 + position={position} 42 + {...props} 43 + > 44 + <SelectPrimitive.Viewport className="p-1">{children}</SelectPrimitive.Viewport> 45 + </SelectPrimitive.Content> 46 + </SelectPrimitive.Portal> 47 + )); 48 + SelectContent.displayName = SelectPrimitive.Content.displayName; 49 + 50 + const SelectItem = React.forwardRef< 51 + React.ElementRef<typeof SelectPrimitive.Item>, 52 + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> 53 + >(({ className, children, ...props }, ref) => ( 54 + <SelectPrimitive.Item 55 + ref={ref} 56 + className={cn( 57 + 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-brand-light focus:text-brand data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 58 + className 59 + )} 60 + {...props} 61 + > 62 + <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> 63 + </SelectPrimitive.Item> 64 + )); 65 + SelectItem.displayName = SelectPrimitive.Item.displayName; 66 + 67 + const SelectLabel = React.forwardRef< 68 + React.ElementRef<typeof SelectPrimitive.Label>, 69 + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> 70 + >(({ className, ...props }, ref) => ( 71 + <SelectPrimitive.Label 72 + ref={ref} 73 + className={cn('px-2 py-1.5 text-xs font-semibold text-gray-500', className)} 74 + {...props} 75 + /> 76 + )); 77 + SelectLabel.displayName = SelectPrimitive.Label.displayName; 78 + 79 + const SelectSeparator = React.forwardRef< 80 + React.ElementRef<typeof SelectPrimitive.Separator>, 81 + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> 82 + >(({ className, ...props }, ref) => ( 83 + <SelectPrimitive.Separator 84 + ref={ref} 85 + className={cn('-mx-1 my-1 h-px bg-gray-100', className)} 86 + {...props} 87 + /> 88 + )); 89 + SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 90 + 91 + export { 92 + Select, 93 + SelectGroup, 94 + SelectValue, 95 + SelectTrigger, 96 + SelectContent, 97 + SelectItem, 98 + SelectLabel, 99 + SelectSeparator, 100 + };
+17
tailwind.config.js
··· 4 4 content: ['./src/renderer/**/*.{ts,tsx,js,jsx}'], 5 5 theme: { 6 6 extend: { 7 + colors: { 8 + brand: { 9 + DEFAULT: '#007c70', 10 + dark: '#00635a', 11 + light: '#e6f2f1', 12 + }, 13 + accent: { 14 + DEFAULT: '#ffc107', 15 + light: '#ffe69c', 16 + }, 17 + signal: { 18 + great: '#66b0a9', 19 + ok: '#ffcd39', 20 + bad: '#e06766', 21 + none: '#bfbfbf', 22 + }, 23 + }, 7 24 borderRadius: { 8 25 lg: 'var(--radius)', 9 26 md: 'calc(var(--radius) - 2px)',