docs(examples): Interview design-system and page build scripts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
# Interview design system (PaperCraft page `Design System`)
|
||||
|
||||
Generated by `design_system.py` + `gen_manifest.py`. Main components are top-level frames on the `Design System` page (library area, right of the doc sections, x >= 1560). Documentation sections: `Section / Colors`, `Section / Typography`, `Section / Spacing`, `Section / Radius & Shadow`, `Section / Page Layout`, `Section / Components` (captioned instances of everything).
|
||||
|
||||
## How to use
|
||||
|
||||
1. On your page: `papercraft-cli call import_components '{"fromPageId":"<Design System page id>","pageId":"<your page id>"}'` (skips names already there; if you delete a copy and re-import, its instances lose overrides).
|
||||
2. Place: `<x-paper-instance component="Button / Primary" overrides='{"Label":"Save"}'/>` or `inst_html("Button / Primary", Label="Save")` from pc.py.
|
||||
3. Overrides JSON: key = exact layer name inside the component (first match), value = string (text) or object (CSS camelCase styles, e.g. `{"Label":{"color":"var(--color-error)"}}`). Hide a part: `{"Col 5":{"display":"none"}}`.
|
||||
4. The instance root accepts a `style` attribute: `style="width:720px"`, `style="background-color:var(--color-avatar-purple)"`, `style="opacity:.5"` (disabled).
|
||||
5. Icons inside components cannot be recolored or swapped by overrides (vector layers). That is why the sidebars exist as one variant per active item. For anything else, compose in raw HTML with the recipes below.
|
||||
6. Components hug their content (fixed width where noted). To stretch, set `style="width:Npx"` on the instance. Uppercase labels (table headers, Stat label, sidebar group labels) are typed uppercase: text-transform is not supported.
|
||||
7. Instances are page-local; no variants/properties exist, so every variant is a separate component.
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons (packages/ui Button)
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Button / Primary` | 34px high, padding 0 16px, 14px label | `Label` |
|
||||
| `Button / Primary Small` | 28px high, padding 0 12px, 12px label | `Label` |
|
||||
| `Button / Primary Large` | 40px high, padding 0 20px, 14px label | `Label` |
|
||||
| `Button / Default` | 34px high, padding 0 16px, 14px label | `Label` |
|
||||
| `Button / Default Small` | 28px high, padding 0 12px, 12px label | `Label` |
|
||||
| `Button / Default Large` | 40px high, padding 0 20px, 14px label | `Label` |
|
||||
| `Button / Ghost` | 34px high, padding 0 16px, 14px label | `Label` |
|
||||
| `Button / Ghost Small` | 28px high, padding 0 12px, 12px label | `Label` |
|
||||
| `Button / Ghost Large` | 40px high, padding 0 20px, 14px label | `Label` |
|
||||
| `Button / Danger` | 34px high, padding 0 16px, 14px label | `Label` |
|
||||
| `Button / Danger Small` | 28px high, padding 0 12px, 12px label | `Label` |
|
||||
| `Button / Danger Large` | 40px high, padding 0 20px, 14px label | `Label` |
|
||||
| `Button / Dark` | 34px high, padding 0 16px, 14px label | `Label` |
|
||||
| `Button / Dark Small` | 28px high, padding 0 12px, 12px label | `Label` |
|
||||
| `Button / Dark Large` | 40px high, padding 0 20px, 14px label | `Label` |
|
||||
| `Button / Notification Bell` | AdminShell bell: 36x36, red unread dot | (none) |
|
||||
|
||||
### Buttons (AntD Button)
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Ant Button / Primary` | AntD button 32px | `Label` |
|
||||
| `Ant Button / Primary Small` | AntD button 24px | `Label` |
|
||||
| `Ant Button / Primary Large` | AntD button 40px | `Label` |
|
||||
| `Ant Button / Default` | AntD button 32px | `Label` |
|
||||
| `Ant Button / Default Small` | AntD button 24px | `Label` |
|
||||
| `Ant Button / Default Large` | AntD button 40px | `Label` |
|
||||
| `Ant Button / Danger` | AntD button 32px | `Label` |
|
||||
| `Ant Button / Danger Small` | AntD button 24px | `Label` |
|
||||
| `Ant Button / Danger Large` | AntD button 40px | `Label` |
|
||||
| `Ant Button / Text` | AntD button 32px | `Label` |
|
||||
| `Ant Button / Text Small` | AntD button 24px | `Label` |
|
||||
| `Ant Button / Text Large` | AntD button 40px | `Label` |
|
||||
| `Ant Button / Text Danger` | AntD button 32px | `Label` |
|
||||
| `Ant Button / Text Danger Small` | AntD button 24px | `Label` |
|
||||
| `Ant Button / Text Danger Large` | AntD button 40px | `Label` |
|
||||
|
||||
### Avatar
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Avatar` | 32px circle, initials, colour by name hash | `Initials` |
|
||||
| `Avatar / 30` | 30px circle, initials, colour by name hash | `Initials` |
|
||||
| `Avatar / 34` | 34px circle, initials, colour by name hash | `Initials` |
|
||||
| `Avatar / 36` | 36px circle, initials, colour by name hash | `Initials` |
|
||||
| `Avatar / 40` | 40px circle, initials, colour by name hash | `Initials` |
|
||||
|
||||
### Card
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Card` | Card: 24px padding, radius-lg, shadow-sm | `Card Title`, `Card Body` |
|
||||
| `Card / Hover` | hover: shadow-md, blue border | `Card Title`, `Card Body` |
|
||||
| `Card / Compact` | pad=md (16px) | `Card Title`, `Card Body` |
|
||||
|
||||
### Sparkline
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Sparkline / Blue` | 72x30 line + gradient area | (none) |
|
||||
| `Sparkline / Purple` | 72x30 line + gradient area | (none) |
|
||||
| `Sparkline / Orange` | 72x30 line + gradient area | (none) |
|
||||
| `Sparkline / Green` | 72x30 line + gradient area | (none) |
|
||||
| `Sparkline / Teal` | 72x30 line + gradient area | (none) |
|
||||
| `Sparkline / Orange Down` | downward trend | (none) |
|
||||
|
||||
### Stat
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Stat / Blue` | label, value 30/700, sparkline, delta line | `Label`, `Value`, `Delta Arrow`, `Delta`, `Trend` |
|
||||
| `Stat / Purple` | label, value 30/700, sparkline, delta line | `Label`, `Value`, `Delta Arrow`, `Delta`, `Trend` |
|
||||
| `Stat / Orange` | label, value 30/700, sparkline, delta line | `Label`, `Value`, `Delta Arrow`, `Delta`, `Trend` |
|
||||
| `Stat / Green` | label, value 30/700, sparkline, delta line | `Label`, `Value`, `Delta Arrow`, `Delta`, `Trend` |
|
||||
| `Stat / Teal` | label, value 30/700, sparkline, delta line | `Label`, `Value`, `Delta Arrow`, `Delta`, `Trend` |
|
||||
| `Stat / Orange Down` | negative delta: red arrow | `Label`, `Value`, `Delta Arrow`, `Delta`, `Trend` |
|
||||
|
||||
### Tag
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Tag / Blue` | 22px chip, 12px/500 | `Label` |
|
||||
| `Tag Dot / Blue` | with status dot | `Label` |
|
||||
| `Tag / Green` | 22px chip, 12px/500 | `Label` |
|
||||
| `Tag Dot / Green` | with status dot | `Label` |
|
||||
| `Tag / Gold` | 22px chip, 12px/500 | `Label` |
|
||||
| `Tag Dot / Gold` | with status dot | `Label` |
|
||||
| `Tag / Red` | 22px chip, 12px/500 | `Label` |
|
||||
| `Tag Dot / Red` | with status dot | `Label` |
|
||||
| `Tag / Purple` | 22px chip, 12px/500 | `Label` |
|
||||
| `Tag Dot / Purple` | with status dot | `Label` |
|
||||
| `Tag / Cyan` | 22px chip, 12px/500 | `Label` |
|
||||
| `Tag Dot / Cyan` | with status dot | `Label` |
|
||||
| `Tag / Gray` | neutral | `Label` |
|
||||
| `Tag Dot / Gray` | neutral with dot | `Label` |
|
||||
|
||||
### Tag (AntD Tag status colors)
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Ant Tag / Default` | antd <Tag color=...> | `Label` |
|
||||
| `Ant Tag / Success` | antd <Tag color=...> | `Label` |
|
||||
| `Ant Tag / Warning` | antd <Tag color=...> | `Label` |
|
||||
| `Ant Tag / Error` | antd <Tag color=...> | `Label` |
|
||||
| `Ant Tag / Processing` | antd <Tag color=...> | `Label` |
|
||||
|
||||
### Submission Status Badge
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Submission Status Badge / Queued` | SubmissionStatusBadge | `Label` |
|
||||
| `Submission Status Badge / Running` | SubmissionStatusBadge | `Label` |
|
||||
| `Submission Status Badge / Graded` | SubmissionStatusBadge | `Label` |
|
||||
| `Submission Status Badge / Errored` | SubmissionStatusBadge | `Label` |
|
||||
| `Submission Status Badge / Unknown` | SubmissionStatusBadge | `Label` |
|
||||
|
||||
### Pill (filter chip)
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Pill / Active` | filter chip, radius 99 | `Label` |
|
||||
| `Pill / Active With Count` | filter chip, radius 99 | `Label`, `Count` |
|
||||
| `Pill / Inactive` | filter chip, radius 99 | `Label` |
|
||||
| `Pill / Inactive With Count` | filter chip, radius 99 | `Label`, `Count` |
|
||||
|
||||
### Search Input
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Search Input` | 260x34 | `Placeholder` |
|
||||
| `Search Input / Focused` | focus ring | `Placeholder` |
|
||||
| `Search Input / Filled` | with value | `Placeholder` |
|
||||
|
||||
### Segmented (packages/ui)
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Segmented / 2 Options Active 1` | track border-light, active = white + shadow-sm | `Option 1`, `Option 2` |
|
||||
| `Segmented / 2 Options Active 2` | track border-light, active = white + shadow-sm | `Option 1`, `Option 2` |
|
||||
| `Segmented / 3 Options Active 1` | track border-light, active = white + shadow-sm | `Option 1`, `Option 2`, `Option 3` |
|
||||
| `Segmented / 3 Options Active 2` | track border-light, active = white + shadow-sm | `Option 1`, `Option 2`, `Option 3` |
|
||||
| `Segmented / 3 Options Active 3` | track border-light, active = white + shadow-sm | `Option 1`, `Option 2`, `Option 3` |
|
||||
| `Segmented / 4 Options Active 1` | track border-light, active = white + shadow-sm | `Option 1`, `Option 2`, `Option 3`, `Option 4` |
|
||||
| `Segmented / 4 Options Active 2` | track border-light, active = white + shadow-sm | `Option 1`, `Option 2`, `Option 3`, `Option 4` |
|
||||
| `Segmented / 4 Options Active 3` | track border-light, active = white + shadow-sm | `Option 1`, `Option 2`, `Option 3`, `Option 4` |
|
||||
| `Segmented / 4 Options Active 4` | track border-light, active = white + shadow-sm | `Option 1`, `Option 2`, `Option 3`, `Option 4` |
|
||||
|
||||
### Segmented (AntD)
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Ant Segmented / 3 Options Active 1` | antd Segmented (challenges page) | `Option 1`, `Option 2`, `Option 3` |
|
||||
| `Ant Segmented / 3 Options Active 2` | antd Segmented (challenges page) | `Option 1`, `Option 2`, `Option 3` |
|
||||
| `Ant Segmented / 3 Options Active 3` | antd Segmented (challenges page) | `Option 1`, `Option 2`, `Option 3` |
|
||||
|
||||
### Score Bar / Dots / Stars
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Score Bar / High` | 160x6 track; set Fill width in px | (none) |
|
||||
| `Score Bar / Mid` | 160x6 track; set Fill width in px | (none) |
|
||||
| `Score Bar / Low` | 160x6 track; set Fill width in px | (none) |
|
||||
| `Score Dots / 0` | 5 squares 26px + label | (none) |
|
||||
| `Score Dots / 1` | 5 squares 26px + label | `Dot 1 Value`, `Label` |
|
||||
| `Score Dots / 2` | 5 squares 26px + label | `Dot 1 Value`, `Dot 2 Value`, `Label` |
|
||||
| `Score Dots / 3` | 5 squares 26px + label | `Dot 1 Value`, `Dot 2 Value`, `Dot 3 Value`, `Label` |
|
||||
| `Score Dots / 4` | 5 squares 26px + label | `Dot 1 Value`, `Dot 2 Value`, `Dot 3 Value`, `Dot 4 Value`, `Label` |
|
||||
| `Score Dots / 5` | 5 squares 26px + label | `Dot 1 Value`, `Dot 2 Value`, `Dot 3 Value`, `Dot 4 Value`, `Dot 5 Value`, `Label` |
|
||||
| `Stars / 0` | ★ gold / ☆ border | (none) |
|
||||
| `Stars / 1` | ★ gold / ☆ border | (none) |
|
||||
| `Stars / 2` | ★ gold / ☆ border | (none) |
|
||||
| `Stars / 3` | ★ gold / ☆ border | (none) |
|
||||
| `Stars / 4` | ★ gold / ☆ border | (none) |
|
||||
| `Stars / 5` | ★ gold / ☆ border | (none) |
|
||||
|
||||
### Donut / Spin / Empty / Loading
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Donut` | 132px ring, 16px stroke; segments = stroke colors | `Total`, `Caption` |
|
||||
| `Spin / Small` | antd Spin small | (none) |
|
||||
| `Spin` | antd Spin default | (none) |
|
||||
| `Spin / Large` | antd Spin large | (none) |
|
||||
| `Loading Block` | LoadingBlock: Spin + tip | `Tip` |
|
||||
| `Empty / Simple` | antd Empty (simple image) | `Description` |
|
||||
| `Empty State` | EmptyState: title + description | `Title`, `Description` |
|
||||
|
||||
### Page headers
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Page Header` | PageHeader w/o actions (1192 = 1440 - sidebar) | `Title`, `Subtitle` |
|
||||
| `Admin Shell Header` | AdminShell header: title, subtitle, bell (instance of Button / Notification Bell) | `Title`, `Subtitle` |
|
||||
| `Candidate Shell Header` | CandidateShell header (white card inset on layout bg) | `Title`, `Subtitle`, `Exit Label` |
|
||||
| `Section Title` | SectionTitle: 16/600 + 12 sub | `Title`, `Subtitle` |
|
||||
| `Section Title / With Action` | with action slot | `Title`, `Subtitle`, `Action` |
|
||||
|
||||
### Data Table (packages/ui DataTable)
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Data Table / Header Row` | uppercase 12/600; hide unused Col n with display:none | `Col 1`, `Col 2`, `Col 3`, `Col 4`, `Col 5` |
|
||||
| `Data Table / Row` | 13px vertical padding in code (comfortable) | `Cell 1`, `Cell 2`, `Cell 3`, `Cell 4`, `Cell 5` |
|
||||
| `Data Table / Row Hover` | 13px vertical padding in code (comfortable) | `Cell 1`, `Cell 2`, `Cell 3`, `Cell 4`, `Cell 5` |
|
||||
| `Pagination` | antd Pagination: 32px items, active = blue border | `Page Label` |
|
||||
| `Data Table / Footer` | summary + pagination | `Summary`, `Page Label` |
|
||||
| `Data Table / Empty` | emptyMessage, 48px padding | `Message` |
|
||||
|
||||
### Ant Table
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Ant Table / Header Row` | antd Table th (#fafafa, 14/600) | `Col 1`, `Col 2`, `Col 3`, `Col 4`, `Col 5` |
|
||||
| `Ant Table / Row` | antd Table td, row ~55px | `Cell 1`, `Cell 2`, `Cell 3`, `Cell 4`, `Cell 5` |
|
||||
|
||||
### Input (AntD)
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Input` | 32px, radius 8, border #d9d9d9 | `Value` |
|
||||
| `Input / Filled` | with value | `Value` |
|
||||
| `Input / Focus` | focus ring | `Value` |
|
||||
| `Input / Error` | error state | `Value` |
|
||||
| `Input / Disabled` | disabled | `Value` |
|
||||
| `Input / Large` | size=large: 40px / 16px (forms) | `Value` |
|
||||
| `Input / Large Filled` | size=large with value | `Value` |
|
||||
| `Input / Large Focus` | size=large focus | `Value` |
|
||||
| `Input / Large Error` | size=large error | `Value` |
|
||||
| `Input / With Prefix` | Workspace URL field | `Prefix`, `Value` |
|
||||
| `Input / Password` | Input.Password large | `Value` |
|
||||
| `Input / Search` | Input.Search | `Value` |
|
||||
| `Input Number` | InputNumber (steppers on the right) | `Value` |
|
||||
| `Date Picker` | DatePicker | `Value` |
|
||||
| `Textarea` | Input.TextArea, 3 rows | `Value` |
|
||||
| `Textarea / Filled` | filled | `Value` |
|
||||
|
||||
### Select (AntD)
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Select` | Select closed | `Value` |
|
||||
| `Select / Filled` | Select closed | `Value` |
|
||||
| `Select / Large` | Select closed | `Value` |
|
||||
| `Select / Large Filled` | Select closed | `Value` |
|
||||
| `Select / Dropdown` | open options panel; selected = bg-selected + 600 | `Option 1 Label`, `Option 2 Label`, `Option 3 Label` |
|
||||
|
||||
### Checkbox / Radio / Switch
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Checkbox / Unchecked` | 16px box | `Label` |
|
||||
| `Checkbox / Checked` | 16px box | `Label` |
|
||||
| `Radio / Unselected` | radio | `Label` |
|
||||
| `Radio / Selected` | radio | `Label` |
|
||||
| `Switch / Off` | 44x22 | (none) |
|
||||
| `Switch / On` | 44x22 | (none) |
|
||||
|
||||
### Form Field
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Form Field` | label + large input (AntD Form.Item vertical) | `Label`, `Value` |
|
||||
| `Form Field / Required` | * required mark | `Required Mark`, `Label`, `Value` |
|
||||
| `Form Field / Error` | error + message | `Required Mark`, `Label`, `Value`, `Help` |
|
||||
| `Form Field / With Help` | help text | `Label`, `Value`, `Help` |
|
||||
|
||||
### Overlays
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Modal` | antd Modal; put on a bg-overlay frame | `Title`, `Body`, `Cancel Label`, `OK Label` |
|
||||
| `Drawer` | antd Drawer (right, full height) | `Title`, `Body` |
|
||||
| `Tooltip` | antd Tooltip | `Text` |
|
||||
|
||||
### Alert
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Alert / Info` | antd Alert | `Message` |
|
||||
| `Alert / Info With Icon` | antd Alert | `Message` |
|
||||
| `Alert / Success` | antd Alert | `Message` |
|
||||
| `Alert / Success With Icon` | antd Alert | `Message` |
|
||||
| `Alert / Warning` | antd Alert | `Message` |
|
||||
| `Alert / Warning With Icon` | antd Alert | `Message` |
|
||||
| `Alert / Error` | antd Alert | `Message` |
|
||||
| `Alert / Error With Icon` | antd Alert | `Message` |
|
||||
|
||||
### Tabs
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Tabs / Tab Active` | antd Tabs item (ink bar when active) | `Label` |
|
||||
| `Tabs / Tab Active Large` | antd Tabs item (ink bar when active) | `Label` |
|
||||
| `Tabs / Tab Inactive` | antd Tabs item (ink bar when active) | `Label` |
|
||||
| `Tabs / Tab Inactive Large` | antd Tabs item (ink bar when active) | `Label` |
|
||||
| `Tabs / Bar 3 Active 1` | antd Tabs bar, 14px | `Tab 1 Label`, `Tab 2 Label`, `Tab 3 Label` |
|
||||
| `Tabs / Bar 3 Active 2` | antd Tabs bar, 14px | `Tab 1 Label`, `Tab 2 Label`, `Tab 3 Label` |
|
||||
| `Tabs / Bar 3 Active 3` | antd Tabs bar, 14px | `Tab 1 Label`, `Tab 2 Label`, `Tab 3 Label` |
|
||||
| `Tabs / Bar 5 Large Active 1` | configuration page tabs (size=large) | `Tab 1 Label`, `Tab 2 Label`, `Tab 3 Label`, `Tab 4 Label`, `Tab 5 Label` |
|
||||
| `Tabs / Bar 5 Large Active 2` | configuration page tabs (size=large) | `Tab 1 Label`, `Tab 2 Label`, `Tab 3 Label`, `Tab 4 Label`, `Tab 5 Label` |
|
||||
| `Tabs / Bar 5 Large Active 3` | configuration page tabs (size=large) | `Tab 1 Label`, `Tab 2 Label`, `Tab 3 Label`, `Tab 4 Label`, `Tab 5 Label` |
|
||||
| `Tabs / Bar 5 Large Active 4` | configuration page tabs (size=large) | `Tab 1 Label`, `Tab 2 Label`, `Tab 3 Label`, `Tab 4 Label`, `Tab 5 Label` |
|
||||
| `Tabs / Bar 5 Large Active 5` | configuration page tabs (size=large) | `Tab 1 Label`, `Tab 2 Label`, `Tab 3 Label`, `Tab 4 Label`, `Tab 5 Label` |
|
||||
|
||||
### Misc AntD
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Progress / Circle` | Progress type=circle size=64 | `Value` |
|
||||
| `Skeleton` | antd Skeleton active (static) | (none) |
|
||||
|
||||
### App shell
|
||||
|
||||
| Component (exact name) | Looks like | Text layers (override keys) |
|
||||
|---|---|---|
|
||||
| `Admin Sidebar` | 248x900 dark nav; no active item | `Brand Mark Letter`, `Brand Name`, `Brand Sub`, `Group Label / Workspace`, `Nav Label / Dashboard`, `Nav Label / Challenges`, `Nav Label / Question bank`, `Nav Label / Sessions`, `Nav Label / Schedule`, `Group Label / Manage`, `Nav Label / Configuration`, `Nav Label / Rubric`, `User Initials`, `User Name`, `User Role Tag Label`, `User Email` |
|
||||
| `Admin Sidebar / Dashboard Active` | Dashboard highlighted | `Brand Mark Letter`, `Brand Name`, `Brand Sub`, `Group Label / Workspace`, `Nav Label / Dashboard`, `Nav Label / Challenges`, `Nav Label / Question bank`, `Nav Label / Sessions`, `Nav Label / Schedule`, `Group Label / Manage`, `Nav Label / Configuration`, `Nav Label / Rubric`, `User Initials`, `User Name`, `User Role Tag Label`, `User Email` |
|
||||
| `Admin Sidebar / Challenges Active` | Challenges highlighted | `Brand Mark Letter`, `Brand Name`, `Brand Sub`, `Group Label / Workspace`, `Nav Label / Dashboard`, `Nav Label / Challenges`, `Nav Label / Question bank`, `Nav Label / Sessions`, `Nav Label / Schedule`, `Group Label / Manage`, `Nav Label / Configuration`, `Nav Label / Rubric`, `User Initials`, `User Name`, `User Role Tag Label`, `User Email` |
|
||||
| `Admin Sidebar / Question bank Active` | Question bank highlighted | `Brand Mark Letter`, `Brand Name`, `Brand Sub`, `Group Label / Workspace`, `Nav Label / Dashboard`, `Nav Label / Challenges`, `Nav Label / Question bank`, `Nav Label / Sessions`, `Nav Label / Schedule`, `Group Label / Manage`, `Nav Label / Configuration`, `Nav Label / Rubric`, `User Initials`, `User Name`, `User Role Tag Label`, `User Email` |
|
||||
| `Admin Sidebar / Sessions Active` | Sessions highlighted | `Brand Mark Letter`, `Brand Name`, `Brand Sub`, `Group Label / Workspace`, `Nav Label / Dashboard`, `Nav Label / Challenges`, `Nav Label / Question bank`, `Nav Label / Sessions`, `Nav Label / Schedule`, `Group Label / Manage`, `Nav Label / Configuration`, `Nav Label / Rubric`, `User Initials`, `User Name`, `User Role Tag Label`, `User Email` |
|
||||
| `Admin Sidebar / Schedule Active` | Schedule highlighted | `Brand Mark Letter`, `Brand Name`, `Brand Sub`, `Group Label / Workspace`, `Nav Label / Dashboard`, `Nav Label / Challenges`, `Nav Label / Question bank`, `Nav Label / Sessions`, `Nav Label / Schedule`, `Group Label / Manage`, `Nav Label / Configuration`, `Nav Label / Rubric`, `User Initials`, `User Name`, `User Role Tag Label`, `User Email` |
|
||||
| `Admin Sidebar / Configuration Active` | Configuration highlighted | `Brand Mark Letter`, `Brand Name`, `Brand Sub`, `Group Label / Workspace`, `Nav Label / Dashboard`, `Nav Label / Challenges`, `Nav Label / Question bank`, `Nav Label / Sessions`, `Nav Label / Schedule`, `Group Label / Manage`, `Nav Label / Configuration`, `Nav Label / Rubric`, `User Initials`, `User Name`, `User Role Tag Label`, `User Email` |
|
||||
| `Admin Sidebar / Rubric Active` | Rubric highlighted | `Brand Mark Letter`, `Brand Name`, `Brand Sub`, `Group Label / Workspace`, `Nav Label / Dashboard`, `Nav Label / Challenges`, `Nav Label / Question bank`, `Nav Label / Sessions`, `Nav Label / Schedule`, `Group Label / Manage`, `Nav Label / Configuration`, `Nav Label / Rubric`, `User Initials`, `User Name`, `User Role Tag Label`, `User Email` |
|
||||
| `Candidate Sidebar` | 248x900; no active item | `Brand Name`, `Brand Sub`, `Nav Label / Home`, `Nav Label / My interview`, `User Initials`, `User Name`, `User Email` |
|
||||
| `Candidate Sidebar / Home Active` | Home highlighted | `Brand Name`, `Brand Sub`, `Nav Label / Home`, `Nav Label / My interview`, `User Initials`, `User Name`, `User Email` |
|
||||
| `Candidate Sidebar / My interview Active` | My interview highlighted | `Brand Name`, `Brand Sub`, `Nav Label / Home`, `Nav Label / My interview`, `User Initials`, `User Name`, `User Email` |
|
||||
|
||||
## Style-override tips
|
||||
|
||||
- **Recolor an Avatar**: `style="background-color:var(--color-avatar-purple)"` on the instance (palette: brand-primary, avatar-purple, avatar-teal, warning, avatar-pink, success, avatar-orange, avatar-indigo; the app picks by a hash of the name). Set initials with `{"Initials":"PI"}`.
|
||||
- **Active sidebar item**: use the variant `Admin Sidebar / <Item> Active` (Dashboard, Challenges, Question bank, Sessions, Schedule, Configuration, Rubric) or `Candidate Sidebar / Home Active` / `My interview Active`. Plain `Admin Sidebar` / `Candidate Sidebar` have no active item. Manual route (text/bg only; icon stays gray): `{"Nav / Sessions":{"backgroundColor":"var(--color-brand-primary)"},"Nav Label / Sessions":{"color":"var(--color-text-inverse)","fontWeight":600}}`. User name/email: `User Name`, `User Email`, `User Initials`, `User Role Tag Label`.
|
||||
- **Score Bar**: set `Fill` width in px of the 160px track: `{"Fill":{"width":"100px"}}`; recolor `{"Fill":{"backgroundColor":"var(--color-avatar-purple)"}}`; thinner: `{"Fill":{"height":"4px"}}` plus root `style="height:4px"`.
|
||||
- **Segmented / Ant Segmented**: pick the variant whose `Active N` matches; edit `Option 1..n`.
|
||||
- **Tabs / Bar**: pick `Active N` variant; edit `Tab N Label`. Bars have a 1px divider under them.
|
||||
- **Stat**: `Label` (uppercase), `Value`, `Delta`, `Trend`, `Delta Arrow` (use `Stat / Orange Down` for a negative delta). Sparkline shape is fixed.
|
||||
- **Pill / Tag / Badge status colors**: choose the tone component. Custom: `{"Label":{"color":"var(--color-tag-red-fg)"}}` and root `style="background-color:var(--color-tag-red-bg);border:1px solid var(--color-tag-red-bd)"`.
|
||||
- **Data Table / Header Row, Row, Ant Table rows**: 5 equal flex cells `Col 1..5` / `Cell 1..5`; for fewer columns hide the trailing ones with display:none; width via `style`. For rows with Tags/Avatars compose raw HTML in a `<table>` and reuse `Tag / ...` instances in cells. Wrap a table in a Card-like frame: radius-lg, 1px border-light, overflow hidden.
|
||||
- **Inputs**: `Value` is the text (placeholder color by default; use `Input / Filled` for values). Width via instance style. `Form Field*` has `Label`, `Required Mark`, `Value`, `Help`.
|
||||
- **Disabled button**: `style="opacity:.5"` on the instance. **Block button**: `style="width:100%"` (add `justify-content:center`: already centered).
|
||||
- **Card with gradient** (dashboard Awaiting review): build raw: `background:linear-gradient(180deg,#fffdf5,#ffffff)`.
|
||||
|
||||
## Recipes for raw HTML (when no component fits)
|
||||
|
||||
- Card: `display:flex;flex-direction:column;padding:var(--spacing-lg);background:var(--color-bg-elevated);border:1px solid var(--color-border-light);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm)`.
|
||||
- Button with icon: row, gap 8px, height 34px (sm 28, lg 40), padding 0 16px, radius-md; icon 14-16px via `<x-paper-icon name="phosphor:plus" size="16" color="var(--color-text-inverse)"/>`.
|
||||
- Page frame 1440x900: left `Admin Sidebar ...` (248x900) + main column 1192 wide: `Admin Shell Header` (about 102px tall) + content (padding 24, bg-layout, max-width 900/1100 centered).
|
||||
- Modal overlay: full-screen frame `background:var(--color-bg-overlay)` with a `Modal` instance centered.
|
||||
- Section titles inside cards: `Section Title` / `Section Title / With Action` instances.
|
||||
|
||||
## Variables (use as `var(--name)`)
|
||||
|
||||
Core tokens exist in the project (from tokens.css, same names minus the leading `--`): color-brand-primary, color-brand-secondary, color-bg-layout, color-bg-elevated, color-bg-sider, color-bg-selected, color-bg-overlay, color-border, color-border-light, color-border-strong, color-text-primary, color-text-secondary, color-text-disabled, color-text-inverse, color-text-link, color-success, color-warning, color-error, color-info, color-diff-added, color-diff-removed, color-diff-neutral, color-code-bg, color-code-fg; spacing-xs/sm/md/lg/xl/xxl (4/8/16/24/32/48), page-padding (24), page-padding-lg (48), page-max-width-xs/sm/md/lg/xl/narrow/default/wide (480/420/720/900/1100/720/900/1100), font-size-xs..xxl (12/14/16/18/20/24), radius-sm/md/lg (4/8/12), shadow-sm/md/lg, z-base/dropdown/modal/toast (1/1000/1050/1100).
|
||||
|
||||
Note: multi-value shorthands (`padding:var(--spacing-md) var(--page-padding)`) are NOT resolved by write_html: use single-value padding with var, or literal px for shorthands. Single `var(--spacing-*)`, `var(--radius-*)`, `var(--shadow-*)`, `var(--font-size-*)` work.
|
||||
|
||||
### Extra color variables added by the Design System page
|
||||
|
||||
- **Brand**: `color-button-primary-hover` #4096ff
|
||||
- **Border**: `color-card-hover-border` #bfdbff
|
||||
- **Text**: `color-text-placeholder` #00000040
|
||||
- **Status**: `color-star` #faad14, `color-score-warn` #faad14, `color-score-poor` #ff4d4f
|
||||
- **Sidebar (dark shell)**: `color-sidebar-bg` #11161d, `color-sidebar-border` #ffffff12, `color-sidebar-text` #ffffffa6, `color-sidebar-text-muted` #ffffff73, `color-sidebar-text-faint` #ffffff59, `color-sidebar-hover` #ffffff0f, `color-sidebar-badge` #ffffff40, `color-sidebar-tag-bg` #1677ff40
|
||||
- **Tag tones**: `color-tag-blue-fg` #0958d9, `color-tag-blue-bg` #e6f4ff, `color-tag-blue-bd` #91caff, `color-tag-green-fg` #389e0d, `color-tag-green-bg` #f6ffed, `color-tag-green-bd` #b7eb8f, `color-tag-gold-fg` #ad6800, `color-tag-gold-bg` #fffbe6, `color-tag-gold-bd` #ffe58f, `color-tag-red-fg` #cf1322, `color-tag-red-bg` #fff1f0, `color-tag-red-bd` #ffccc7, `color-tag-purple-fg` #531dab, `color-tag-purple-bg` #f9f0ff, `color-tag-purple-bd` #d3adf7, `color-tag-cyan-fg` #08979c, `color-tag-cyan-bg` #e6fffb, `color-tag-cyan-bd` #87e8de
|
||||
- **Avatar palette**: `color-avatar-purple` #722ed1, `color-avatar-teal` #13a8a8, `color-avatar-pink` #eb2f96, `color-avatar-orange` #fa541c, `color-avatar-indigo` #2f54eb
|
||||
- **Controls**: `color-button-dark` #1f1f1f, `color-tooltip-bg` #000000d9, `color-switch-off` #00000040, `color-focus-ring` #1677ff1a
|
||||
|
||||
### Font stack and type scale
|
||||
|
||||
Inter only (regular/bold; weights 500 render regular, 600+ bold). Sizes: 12 labels/tags/table headers, 14 body, 16 section/card title, 24 page title (600), 30 stat value (700), 15/16 brand name.
|
||||
|
||||
## Known deviations from the source
|
||||
|
||||
- Pill in the code is the filter chip (active/inactive); there are no tone variants (`Pill / Success` does not exist). Tones are `Tag`.
|
||||
- `lint_design`: remaining errors/warnings are faithful brand colors (white on #1677ff 4.1:1, success green on white, etc.), control heights under 44px (34/32px as in the source), and svg-internal names. Not changed on purpose.
|
||||
- Text under 12px in the source (10.5/11) is rendered at 12; padding off the 4px grid was snapped (11->12, 18->16, 14->16, 9->0 with fixed 36px item height).
|
||||
- Main components cannot be nested in the `Section / Components` frame (create_component/import_components need top-level frames), so the section shows instances; masters are in the library area.
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Shared helpers for the Admin > Sessions scripts (admin_sessions.py and its adm_*.py modules)."""
|
||||
import json, sys
|
||||
|
||||
sys.path.insert(0, "/Volumes/Storage/Projects/craft-skills/skills/papercraft/scripts")
|
||||
from pc import * # noqa: F401,F403,E402
|
||||
|
||||
PROJECT = "Interview"
|
||||
SP = {4: "xs", 8: "sm", 16: "md", 24: "lg", 32: "xl", 48: "xxl"}
|
||||
FS = {12: "xs", 14: "sm", 16: "md", 18: "lg", 20: "xl", 24: "xxl"}
|
||||
LH = {10: 14, 11: 16, 12: 16, 13: 18, 14: 22, 15: 20, 16: 24, 18: 26, 20: 28, 22: 28, 24: 32, 30: 36}
|
||||
|
||||
# IDE / code surfaces use literal hexes in the source; they get variables so nothing is hard-coded here.
|
||||
IDE_VARS = {
|
||||
"color-ide-toolbar": "#252526", "color-ide-border": "#1a1a1a", "color-ide-code-pane": "#161616",
|
||||
"color-ide-preview": "#0d1117", "color-ide-muted": "#888888", "color-ide-faint": "#555555",
|
||||
"color-ide-chip": "#2a2a2a", "color-ide-wb": "#13131f", "color-ide-wb-bar": "#1c1c2e",
|
||||
"color-ide-wb-border": "#252540", "color-ide-card": "#161616", "color-ide-card-border": "#252525",
|
||||
"color-ide-card-text": "#dddddd", "color-ide-card-label": "#484848", "color-ide-gutter": "#5a5a5a",
|
||||
"color-ide-err-bg": "#1a0808", "color-ide-err-border": "#3a1010", "color-ide-del-bg": "#3a1a1a",
|
||||
"color-wb-white": "#e0e0e0", "color-wb-blue": "#6ab0ff", "color-wb-amber": "#d29922", "color-wb-purple": "#c792ea",
|
||||
"color-wb-orange": "#ff9f43", "color-ide-wb-tool": "#9999bb", "color-ide-wb-btn": "#252540", "color-ide-sel": "#2a2d2e",
|
||||
"color-ide-console": "#181818", "color-tok-comment": "#6a9955", "color-tok-string": "#ce9178", "color-tok-keyword": "#569cd6",
|
||||
"color-tok-type": "#4ec9b0", "color-tok-number": "#b5cea8", "color-tok-fn": "#dcdcaa",
|
||||
}
|
||||
STATUS = { # label: Tag Dot tone
|
||||
"Draft": "Gray", "Open": "Blue", "In progress": "Gold", "Submitted": "Purple", "Graded": "Green", "Cancelled": "Red",
|
||||
}
|
||||
|
||||
|
||||
def sp(n):
|
||||
return f"var(--spacing-{SP[n]})" if n in SP else f"{n}px"
|
||||
|
||||
|
||||
def fs(n):
|
||||
return f"var(--font-size-{FS[n]})" if n in FS else f"{n}px"
|
||||
|
||||
|
||||
def rad(n):
|
||||
return {4: "var(--radius-sm)", 8: "var(--radius-md)", 12: "var(--radius-lg)"}.get(n, f"{n}px")
|
||||
|
||||
|
||||
def T(name, t, size=14, color="var(--color-text-primary)", weight=400, lh=None, **st):
|
||||
return text(name, t, size=fs(size), color=color, weight=weight, line_height=f"{lh or LH.get(size, round(size * 1.4))}px", **st)
|
||||
|
||||
|
||||
def ic(name, size=16, color="var(--color-text-secondary)", layer=None):
|
||||
return f'<x-paper-icon name="phosphor:{name}" size="{size}" color="{color}" layer-name="{layer or "Icon / " + name}"/>'
|
||||
|
||||
|
||||
def inst(component, name=None, style="", **ov):
|
||||
o = json.dumps(ov, ensure_ascii=False).replace("'", "'")
|
||||
n = f' layer-name="{name}"' if name else ""
|
||||
s = f' style="{style}"' if style else ""
|
||||
return f"<x-paper-instance component=\"{component}\"{n}{s} overrides='{o}'/>"
|
||||
|
||||
|
||||
def setup():
|
||||
fid = next(i["id"] for i in call("list_resources")["items"] if i["name"] == PROJECT)
|
||||
ps = pages(fid)
|
||||
A = ps["Admin"]
|
||||
call("set_variables", {"fileId": fid, "variables": IDE_VARS})
|
||||
call("import_components", {"fileId": fid, "fromPageId": ps["Design System"].id, "pageId": A.id})
|
||||
return fid, A
|
||||
|
||||
|
||||
def reset(A, name):
|
||||
ids = [c["id"] for c in A.c("get_children", nodeId="root")["children"] if c["name"] == name]
|
||||
if ids:
|
||||
A.c("delete_nodes", nodeIds=ids)
|
||||
|
||||
|
||||
def section(A, name, title, per_row=3):
|
||||
reset(A, name)
|
||||
w = per_row * 1440 + (per_row - 1) * 32 + 64
|
||||
sid = A.artboard(name, display="flex", flex_direction="row", flex_wrap="wrap", gap="32px", padding="32px",
|
||||
width=f"{w}px", height="fit-content", background_color="transparent", align_content="flex-start")
|
||||
A.add(sid, T("Section Title", title, 24, weight=600, width="100%"))
|
||||
return sid
|
||||
|
||||
|
||||
def add_screen(A, sid, name, route, body, **frame):
|
||||
"""body = inner HTML of the 1440x900 screen frame."""
|
||||
st = dict(display="flex", width="1440px", height="900px", background_color="var(--color-bg-layout)", overflow="hidden")
|
||||
st.update(frame)
|
||||
html = col(f"Screen Slot / {name}", T("Route", route, 12, "var(--color-text-secondary)") + box(f"Screen / {name}", body, **st),
|
||||
gap="8px", width="1440px")
|
||||
r = A.c("write_html", targetNodeId=sid, mode="insert-children", html=html)
|
||||
for k in ("warnings", "ignoredStyles"):
|
||||
if r.get(k):
|
||||
print(f" [{name}] {k}:", sorted(set(map(str, r[k])))[:8])
|
||||
return r["createdNodeIds"][0]
|
||||
|
||||
|
||||
USER = dict(**{"User Name": "Elena Vasquez", "User Email": "elena@netcracker.com", "User Initials": "EV"})
|
||||
|
||||
|
||||
def sidebar(active="Sessions"):
|
||||
return inst(f"Admin Sidebar / {active} Active", "Sidebar", **USER)
|
||||
|
||||
|
||||
def shell(title, subtitle, content, actions="", wide=True):
|
||||
"""Admin shell screen body: sidebar + main (header, content). `actions` are absolute-positioned header buttons."""
|
||||
main = col("Main", inst("Admin Shell Header", "Header", Title=title, Subtitle=subtitle) + actions + content,
|
||||
flex_grow="1", height="900px", position="relative", overflow="hidden")
|
||||
return sidebar() + main
|
||||
|
||||
|
||||
def page_content(inner, wide=True):
|
||||
w = 1100 if wide else 900
|
||||
return row("Content Wrap", col("Content", inner, width=f"{w}px", padding="24px", gap="16px"), justify_content="center",
|
||||
align_items="flex-start", flex_grow="1", width="1192px")
|
||||
|
||||
|
||||
def header_action(label_component, label, width=108, right=72):
|
||||
return f'<div layer-name="Header Actions" style="position:absolute;left:{1192-right-width}px;top:34px;display:flex;gap:12px">' + \
|
||||
inst(label_component, label.replace("_", " "), Label=label) + "</div>"
|
||||
|
||||
|
||||
def card(name, inner, **st):
|
||||
base = dict(display="flex", flex_direction="column", background_color="var(--color-bg-elevated)",
|
||||
border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)", overflow="hidden")
|
||||
base.update(st)
|
||||
return box(name, inner, **base)
|
||||
@@ -0,0 +1,141 @@
|
||||
"""IDE-style read-only surfaces shared by Live Session and Review (CodeEditor, Mermaid preview, Whiteboard, Theory view)."""
|
||||
import re
|
||||
from adm_common import *
|
||||
|
||||
TOK = {"comment": "var(--color-tok-comment)", "string": "var(--color-tok-string)", "keyword": "var(--color-tok-keyword)",
|
||||
"type": "var(--color-tok-type)", "number": "var(--color-tok-number)", "fn": "var(--color-tok-fn)", "ident": "var(--color-code-fg)"}
|
||||
KW = re.compile(r"^(?:import|from|export|default|const|let|var|function|return|if|else|for|while|await|async|new|class|interface|type|of|in|try|catch|throw|null|undefined|true|false|void|this)\b")
|
||||
RULES = [("comment", re.compile(r"^//.*")), ("string", re.compile(r"""^`[^`]*`?|^"[^"]*"?|^'[^']*'?""")), ("keyword", KW),
|
||||
("type", re.compile(r"^[A-Z][A-Za-z0-9_]*")), ("number", re.compile(r"^\d+(?:\.\d+)?")),
|
||||
("fn", re.compile(r"^[a-zA-Z_$][\w$]*(?=\s*\()")), ("ident", re.compile(r"^[a-zA-Z_$][\w$]*")), ("ws", re.compile(r"^\s+"))]
|
||||
|
||||
|
||||
def esc(t):
|
||||
return t.replace("&", "&").replace("<", "<").replace(">", ">").replace(" ", " ")
|
||||
|
||||
|
||||
def tokenize(line):
|
||||
out, i = "", 0
|
||||
while i < len(line):
|
||||
s = line[i:]
|
||||
for cls, rx in RULES:
|
||||
m = rx.match(s)
|
||||
if m:
|
||||
t = m.group(0)
|
||||
out += esc(t) if cls == "ws" else f'<span style="color:{TOK[cls]}">{esc(t)}</span>'
|
||||
i += len(t)
|
||||
break
|
||||
else:
|
||||
out += esc(line[i]); i += 1
|
||||
return out
|
||||
|
||||
|
||||
def code_editor(code, height=None, size=13, name="Code Editor"):
|
||||
"""Read-only CodeEditor: gutter + highlighted lines on color-code-bg."""
|
||||
lines = code.split("\n")
|
||||
lh = round(size * 1.55)
|
||||
gut = "".join(T(f"Ln {i+1}", str(i + 1), size, "var(--color-ide-gutter)", lh=lh, text_align="right") for i in range(len(lines)))
|
||||
src = "".join(box(f"Line {i+1}", tokenize(l) or " ", font_size=f"{size}px", line_height=f"{lh}px", color="var(--color-code-fg)") for i, l in enumerate(lines))
|
||||
st = dict(background_color="var(--color-code-bg)", width="100%", overflow="hidden", padding="12px")
|
||||
if height:
|
||||
st["height"] = f"{height}px"
|
||||
return row(name, col("Gutter", gut, width="32px", flex_shrink="0") + col("Source", src, flex_grow="1", padding="0 16px"), align_items="flex-start", **st)
|
||||
|
||||
|
||||
TWO_SUM = """// Two Sum: one pass with a hash map, O(n)
|
||||
export function twoSum(nums: number[], target: number): number[] {
|
||||
const seen = new Map<number, number>();
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
const need = target - nums[i];
|
||||
if (seen.has(need)) {
|
||||
return [seen.get(need)!, i];
|
||||
}
|
||||
seen.set(nums[i], i);
|
||||
}
|
||||
return [];
|
||||
}"""
|
||||
|
||||
MERMAID = """graph TD
|
||||
Client -->|HTTPS| LB[Load Balancer]
|
||||
LB --> API1[API Server 1]
|
||||
LB --> API2[API Server 2]
|
||||
API1 <--> Cache[(Redis Cache)]
|
||||
API2 <--> Cache
|
||||
API1 --> DB[(PostgreSQL Primary)]
|
||||
API2 --> DB
|
||||
DB -.->|replication| Replica[(Read Replica)]
|
||||
API1 --> Queue([Message Queue])
|
||||
Queue --> Worker[Background Worker]"""
|
||||
|
||||
|
||||
def node(label, accent=False):
|
||||
return row(f"Node / {label}", T("Label", label, 12, "var(--color-ide-card-text)"), padding="8px 16px", justify_content="center",
|
||||
background_color="var(--color-ide-toolbar)", border="1px solid var(--color-ide-muted)", border_radius="var(--radius-sm)")
|
||||
|
||||
|
||||
def arrow(label=""):
|
||||
return row("Edge", ic("arrow-down", 14, "var(--color-ide-muted)") + (T("Edge Label", label, 12, "var(--color-ide-muted)") if label else ""), gap="4px")
|
||||
|
||||
|
||||
def diagram_editor(height, readonly_note="Read-only — candidate's diagram", active=0, name="Diagram Editor"):
|
||||
tpls = ["System design", "Sequence", "Class diagram", "State machine"]
|
||||
chips = "".join(row(f"Template / {t}", T("Label", t, 12, "#fff" if i == active else "var(--color-ide-muted)"), padding="4px 12px",
|
||||
border_radius="5px", background_color="var(--color-brand-primary)" if i == active else "var(--color-ide-chip)")
|
||||
for i, t in enumerate(tpls))
|
||||
bar = row("Template Bar", T("Label", "Template:", 12, "var(--color-ide-faint)") + chips + box("Spacer", "", flex_grow="1") +
|
||||
T("Note", readonly_note, 12, "var(--color-ide-faint)"), gap="8px", padding="8px 16px", background_color="var(--color-ide-toolbar)",
|
||||
border_bottom="1px solid var(--color-ide-border)", width="100%")
|
||||
src = "".join(T(f"Src {i+1}", l.replace(" ", " ") or " ", 13, "var(--color-code-fg)", lh=23) for i, l in enumerate(MERMAID.split("\n")))
|
||||
left = col("Mermaid Source", src + T("Pane Label", "MERMAID", 12, "var(--color-ide-faint)", position="absolute", right="12px", top="8px"),
|
||||
width="50%", padding="16px", background_color="var(--color-ide-code-pane)", border_right="1px solid var(--color-ide-border)", position="relative")
|
||||
api = row("API Row", node("API Server 1") + node("API Server 2"), gap="32px")
|
||||
store = row("Store Row", node("Redis Cache") + node("PostgreSQL Primary") + node("Message Queue"), gap="24px")
|
||||
graph = col("Rendered Graph", node("Client") + arrow("HTTPS") + node("Load Balancer") + arrow() + api + arrow() + store + arrow("replication") +
|
||||
row("Leaf Row", node("Read Replica") + node("Background Worker"), gap="32px"), align_items="center", gap="4px")
|
||||
right = row("Rendered Preview", graph, width="50%", justify_content="center", align_items="flex-start", padding="28px 20px",
|
||||
background_color="var(--color-ide-preview)")
|
||||
return col(name, bar + row("Panes", left + right, flex_grow="1", width="100%", align_items="stretch", height=f"{height - 41}px"),
|
||||
height=f"{height}px", width="100%", background_color="var(--color-code-bg)")
|
||||
|
||||
|
||||
def shape_box(name, x, y, w, h, color, label="", radius=4):
|
||||
return box(f"Shape / {name}", T("Text", label, 14, color, text_align="center", width=f"{w}px") if label else "",
|
||||
position="absolute", left=f"{x}px", top=f"{y}px", width=f"{w}px", height=f"{h}px", border=f"2px solid {color}",
|
||||
border_radius=f"{radius}px", display="flex", align_items="center", justify_content="center")
|
||||
|
||||
|
||||
def whiteboard(height, empty=False, name="Whiteboard"):
|
||||
shapes = ""
|
||||
if empty:
|
||||
shapes = col("Empty Hint", T("Glyph", "◻", 32, "var(--color-ide-faint)", opacity=".3") + T("Hint", "Candidate hasn't drawn anything yet", 12, "var(--color-ide-faint)"),
|
||||
position="absolute", left="0px", top="0px", width="1192px", height=f"{height}px", align_items="center", justify_content="center", gap="8px")
|
||||
else:
|
||||
W, B, G, R, O = ("var(--color-wb-white)", "var(--color-wb-blue)", "var(--color-diff-added)", "var(--color-diff-removed)", "var(--color-wb-orange)")
|
||||
shapes = (shape_box("Client", 80, 120, 140, 64, W, "Client") + shape_box("API", 380, 120, 160, 64, B, "API Gateway") +
|
||||
shape_box("Auth", 380, 300, 160, 64, G, "Auth Service") + shape_box("DB", 720, 120, 160, 64, O, "Postgres", 32) +
|
||||
shape_box("Cache", 720, 300, 160, 64, R, "Redis", 32) +
|
||||
ic("arrow-right", 40, W, "Arrow / Client to API").replace("<x-paper-icon", '<x-paper-icon style="position:absolute;left:250px;top:132px"') +
|
||||
ic("arrow-right", 40, B, "Arrow / API to DB").replace("<x-paper-icon", '<x-paper-icon style="position:absolute;left:570px;top:132px"') +
|
||||
ic("arrow-down", 40, B, "Arrow / API to Auth").replace("<x-paper-icon", '<x-paper-icon style="position:absolute;left:440px;top:210px"') +
|
||||
ic("arrow-right", 40, G, "Arrow / Auth to Cache").replace("<x-paper-icon", '<x-paper-icon style="position:absolute;left:570px;top:312px"') +
|
||||
T("Note", "cache sessions, TTL 15m", 15, "var(--color-wb-purple)", position="absolute", left="700px", top="390px") +
|
||||
row("Read-only Badge", T("Label", "Read-only — candidate's whiteboard", 12, "var(--color-ide-faint)"), position="absolute", right="14px", bottom="10px",
|
||||
padding="4px 12px", background_color="var(--color-ide-wb-bar)", border="1px solid var(--color-ide-wb-border)", border_radius="5px"))
|
||||
return box(name, shapes, position="relative", width="100%", height=f"{height}px", background_color="var(--color-ide-wb)", overflow="hidden")
|
||||
|
||||
|
||||
def theory_view(height, answers=None, name="Theory Answers"):
|
||||
if not answers:
|
||||
return row(name, T("Waiting", "Waiting for candidate answers…", 13, "var(--color-ide-faint)"), width="100%", height=f"{height}px",
|
||||
justify_content="center", align_items="center", background_color="var(--color-code-bg)")
|
||||
cards = "".join(col(f"Answer / {q}", T("Question Id", q.upper(), 12, "var(--color-ide-card-label)", letter_spacing="0.6px") +
|
||||
T("Answer", a, 14, "var(--color-ide-card-text)", lh=24), gap="8px", padding="16px",
|
||||
background_color="var(--color-ide-card)", border="1px solid var(--color-ide-card-border)", border_radius="var(--radius-md)")
|
||||
for q, a in answers)
|
||||
return row(name, col("Answers", cards, gap="16px", width="760px", padding="24px"), width="100%", height=f"{height}px", justify_content="center",
|
||||
align_items="flex-start", background_color="var(--color-code-bg)", overflow="hidden")
|
||||
|
||||
|
||||
THEORY = [("q-event-loop", "The event loop pulls tasks from the queue only when the call stack is empty. Microtasks (promise callbacks) run before the next macrotask, so a resolved promise always logs before a setTimeout(0)."),
|
||||
("q-cap", "In a partition we must choose between consistency and availability. For the booking flow I would pick CP: reject writes on the minority side rather than double-book a slot."),
|
||||
("q-indexes", "A composite index on (tenant_id, created_at) serves the dashboard query. The leading column filters, the second column gives ordered scans without a sort.")]
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Section / Admin / Live Session: interviewer live view (tabs: Diagrams, Whiteboard, Code, Theory; Live / Waiting)."""
|
||||
from adm_ide import *
|
||||
|
||||
TABS = [("Diagrams", "tree-structure"), ("Whiteboard", "selection"), ("Code", "brackets-curly"), ("Theory", "question")]
|
||||
|
||||
|
||||
def toolbar(active, live):
|
||||
tabs = ""
|
||||
for t, i in TABS:
|
||||
on = t == active
|
||||
tabs += row(f"Tab / {t}", ic(i, 14, "#fff" if on else "var(--color-ide-muted)") + T("Label", t, 13, "#fff" if on else "var(--color-ide-muted)", weight=600 if on else 400),
|
||||
gap="8px", padding="0 16px", height="52px", border_bottom=f"2px solid {'var(--color-brand-primary)' if on else 'transparent'}")
|
||||
tag = inst("Ant Tag / Success", "Live", Label="● Live") if live else inst("Ant Tag / Default", "Waiting", Label="Waiting")
|
||||
return row("Live Toolbar", T("Title", "Live session", 13, "#fff", weight=600) + col("Tabs Gap", "", width="8px") + tabs + box("Spacer", "", flex_grow="1") + tag +
|
||||
inst("Ant Button / Primary Small", "End & score", Label="End & score →") + col("Pad", "", width="4px"), gap="16px", padding="0 12px 0 16px",
|
||||
height="52px", width="100%", background_color="var(--color-ide-toolbar)", border_bottom="1px solid var(--color-ide-border)", flex_shrink="0")
|
||||
|
||||
|
||||
def live_shell(active, live, content):
|
||||
return sidebar() + col("Main", toolbar(active, live) + content, flex_grow="1", height="900px", background_color="var(--color-code-bg)", overflow="hidden")
|
||||
|
||||
|
||||
def build(A):
|
||||
sid = section(A, "Section / Admin / Live Session", "Admin / Live Session")
|
||||
H = 848
|
||||
r = "/sessions/[id]/live"
|
||||
add_screen(A, sid, "Live Diagrams", r, live_shell("Diagrams", True, diagram_editor(H)))
|
||||
add_screen(A, sid, "Live Whiteboard", r + " (whiteboard tab)", live_shell("Whiteboard", True, whiteboard(H)))
|
||||
add_screen(A, sid, "Live Whiteboard Waiting", r + " (waiting, nothing drawn)", live_shell("Whiteboard", False, whiteboard(H, empty=True)))
|
||||
add_screen(A, sid, "Live Code", r + " (code tab)", live_shell("Code", True, col("Code Pane", code_editor(TWO_SUM, H), height=f"{H}px", width="100%")))
|
||||
add_screen(A, sid, "Live Theory", r + " (theory tab)", live_shell("Theory", True, theory_view(H, THEORY)))
|
||||
add_screen(A, sid, "Live Theory Waiting", r + " (waiting for answers)", live_shell("Theory", False, theory_view(H)))
|
||||
return sid
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fid, A = setup()
|
||||
print(build(A))
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Section / Admin / Sessions: list (3 states), new session (2 states), session detail (3 states)."""
|
||||
from adm_common import *
|
||||
|
||||
ROWS = [
|
||||
("Senior backend screen — Marcos R.", "K7M2-9QXA", "Graded"),
|
||||
("Frontend pairing — Aisha Khan", "4HTR-B2LN", "In progress"),
|
||||
("Platform engineer loop — Diego Alvarez", "P9WD-33EC", "Submitted"),
|
||||
("Data engineer screen — Lena Fischer", "M3QZ-81VY", "Open"),
|
||||
("Staff SRE system design — Tom Becker", "X6JA-0RKD", "Draft"),
|
||||
("Mobile lead interview — Priya Nair", "C2FS-77HM", "Cancelled"),
|
||||
("Junior full-stack screen — Omar Haddad", "T8LB-45GE", "Graded"),
|
||||
]
|
||||
FILTERS = ["All", "Open", "In progress", "Submitted", "Graded", "Draft"]
|
||||
TH = dict(padding="12px", font_size=fs(12), font_weight="600", color="var(--color-text-secondary)", background_color="var(--color-bg-sider)",
|
||||
border_bottom="1px solid var(--color-border-light)")
|
||||
|
||||
|
||||
def code_chip(c):
|
||||
return box("Code", c, font_size=fs(12), line_height="16px", color="var(--color-text-secondary)", background_color="var(--color-bg-sider)",
|
||||
padding="2px 8px", border_radius="var(--radius-sm)", border="1px solid var(--color-border-light)")
|
||||
|
||||
|
||||
def _cell(tag, inner, w=None, al="left", **extra):
|
||||
st = dict(padding="12px", text_align=al, border_bottom="1px solid var(--color-border-light)")
|
||||
if w:
|
||||
st["width"] = w
|
||||
st.update(extra)
|
||||
return f'<{tag} style="{css(**st)}">{inner}</{tag}>'
|
||||
|
||||
|
||||
def table(rows, hover=None):
|
||||
hd = dict(font_size=fs(12), font_weight="600", color="var(--color-text-secondary)", background_color="var(--color-bg-sider)")
|
||||
head = ('<tr layer-name="Row / Header">' + _cell("th", "SESSION", **hd) + _cell("th", "CODE", "140px", **hd) +
|
||||
_cell("th", "STATUS", "150px", **hd) + _cell("th", " ", "90px", "right", **hd) + "</tr>")
|
||||
body = ""
|
||||
for i, (title, code, st) in enumerate(rows):
|
||||
bg = dict(background_color="var(--color-bg-selected)") if hover == i else {}
|
||||
bd = dict(font_size=fs(14), color="var(--color-text-primary)", **bg)
|
||||
body += (f'<tr layer-name="Row / {title.split(" — ")[0]}">' +
|
||||
_cell("td", f'<span style="font-weight:600">{title}</span>', **bd) + _cell("td", code_chip(code), "140px", **bd) +
|
||||
_cell("td", inst(f"Tag Dot / {STATUS[st]}", "Status", Label=st), "150px", **bd) +
|
||||
_cell("td", '<span style="color:var(--color-brand-primary);font-weight:500">Open →</span>', "90px", "right", **bd) + "</tr>")
|
||||
return card("Data Table", f'<table layer-name="Table" style="width:100%">{head}{body}</table>')
|
||||
|
||||
|
||||
def toolbar(active="All"):
|
||||
pills = "".join(inst("Pill / Active" if f == active else "Pill / Inactive", f"Pill / {f}", Label=f) for f in FILTERS)
|
||||
return row("Toolbar", row("Filters", pills, gap="8px") + inst("Search Input", "Search", style="width:240px", Placeholder="Search by title or code…"),
|
||||
justify_content="space-between", width="100%")
|
||||
|
||||
|
||||
def new_btn():
|
||||
return header_action("Button / Primary", "New session")
|
||||
|
||||
|
||||
def list_screens(A, sid):
|
||||
add_screen(A, sid, "Sessions List", "/sessions",
|
||||
shell("Sessions", "All interview sessions across the team", page_content(toolbar() + table(ROWS)), new_btn()))
|
||||
add_screen(A, sid, "Sessions List Filtered", "/sessions (Graded filter, search active)",
|
||||
shell("Sessions", "All interview sessions across the team",
|
||||
page_content(toolbar("Graded") + table([r for r in ROWS if r[2] == "Graded"], hover=0)), new_btn()))
|
||||
add_screen(A, sid, "Sessions List Loading", "/sessions (loading)",
|
||||
shell("Sessions", "All interview sessions across the team",
|
||||
page_content(toolbar() + row("Loading", inst("Spin / Large", "Spinner"), justify_content="center", padding="48px", width="100%")), new_btn()))
|
||||
add_screen(A, sid, "Sessions List Empty", "/sessions (no sessions)",
|
||||
shell("Sessions", "All interview sessions across the team",
|
||||
page_content(toolbar() + col("Empty", inst("Empty State", "Empty State", Title="No sessions yet",
|
||||
Description="Create a session to invite a candidate and start interviewing.") +
|
||||
inst("Button / Primary", "New session", Label="New session"), align_items="center", gap="16px", padding="32px", width="100%")), new_btn()))
|
||||
|
||||
|
||||
def label_row(label, required=False):
|
||||
return row("Label", (T("Required Mark", "*", 14, "var(--color-error)") if required else "") + T("Label", label, 14), gap="4px")
|
||||
|
||||
|
||||
def field(label, control, required=False, extra=None):
|
||||
return col(f"Form Item / {label}", label_row(label, required) + control + (T("Extra", extra, 14, "var(--color-text-secondary)") if extra else ""), gap="8px", width="100%")
|
||||
|
||||
|
||||
def new_screens(A, sid):
|
||||
title_in = inst("Input / Large", "Title Input", style="width:100%", Value="e.g. Senior backend screen — Marcos R.")
|
||||
date_in = inst("Date Picker", "Expires Picker", style="width:100%", Value="Select date")
|
||||
|
||||
def form(title_ctl, err=None):
|
||||
err_html = box("Alert / Error", row("Alert Row", ic("x-circle", 16, "var(--color-error)") + T("Message", err, 14), gap="8px"),
|
||||
padding="8px 16px", background_color="var(--color-tag-red-bg)", border="1px solid var(--color-tag-red-bd)",
|
||||
border_radius="var(--radius-md)") if err else ""
|
||||
inner = (T("Card Title", "New session", 24, weight=600, lh=32) + err_html +
|
||||
field("Title", title_ctl, True) +
|
||||
field("Invite expires", date_in, False, "Leave blank for the default (7 days from now).") +
|
||||
inst("Ant Button / Primary", "Create", Label="Create"))
|
||||
return page_content(card("Card", col("Card Body", inner, gap="16px", padding="24px"), box_shadow="var(--shadow-sm)"), wide=False)
|
||||
|
||||
add_screen(A, sid, "New Session", "/sessions/new", shell("New session", "Create an invite for a candidate", form(title_in)))
|
||||
add_screen(A, sid, "New Session Error", "/sessions/new (create failed)",
|
||||
shell("New session", "Create an invite for a candidate",
|
||||
form(inst("Input / Large Filled", "Title Input", style="width:100%", Value="Senior backend screen — Marcos R."), "Create failed")))
|
||||
|
||||
|
||||
def detail_card(session, status, picked):
|
||||
chips = "".join(row(f"Selected / {c}", T("Label", c, 14) + ic("x", 12, "var(--color-text-secondary)"), gap="8px",
|
||||
padding="0 8px", height="24px", background_color="var(--color-bg-sider)", border="1px solid var(--color-border-light)",
|
||||
border_radius="var(--radius-sm)") for c in picked)
|
||||
sel = row("Select (multiple)", row("Selected Items", chips, gap="4px", flex_wrap="wrap", flex_grow="1") + ic("caret-down", 12, "var(--color-text-secondary)"),
|
||||
padding="4px 8px", min_height="32px", border="1px solid var(--color-border)", border_radius="var(--radius-md)",
|
||||
background_color="var(--color-bg-elevated)", width="100%", align_items="center")
|
||||
head = row("Card Head", row("Title Space", T("Title", session["title"], 24, weight=600, lh=32) + inst("Ant Tag / Default", "Status", Label=status) +
|
||||
inst("Ant Button / Primary", "Open review", Label="Open review") +
|
||||
inst("Ant Button / Default", "Open live", Label="Open live"), gap="8px"),
|
||||
padding="16px 24px", border_bottom="1px solid var(--color-border-light)", width="100%")
|
||||
meta = col("Meta", row("Invite Code", T("Label", "Invite code:", 14, "var(--color-text-secondary)") + T("Code", session["code"], 16, lh=24), gap="4px") +
|
||||
row("Expires", T("Label", "Expires:", 14, "var(--color-text-secondary)") + T("Value", session["expires"], 14), gap="4px"), gap="8px")
|
||||
body = col("Card Body", meta + col("Challenges", T("Heading", "Challenges", 14, weight=600, lh=22) + sel +
|
||||
inst("Ant Button / Primary", "Save challenges", Label="Save challenges"), gap="12px", width="100%"),
|
||||
gap="16px", padding="24px", width="100%")
|
||||
return card("Card", head + body, box_shadow="var(--shadow-sm)", width="100%")
|
||||
|
||||
|
||||
def detail_screens(A, sid):
|
||||
s = dict(title="Senior backend screen — Marcos R.", code="K7M2-9QXA", expires="2026-10-19T09:00:00Z")
|
||||
add_screen(A, sid, "Session Detail", "/sessions/[id]",
|
||||
shell("Senior backend screen — Marcos R.", "Session · Open", page_content(detail_card(s, "Open", ["Two Sum (CODE)", "System design: URL shortener (DIAGRAM)", "JavaScript Closures (MCQ)"]))))
|
||||
add_screen(A, sid, "Session Detail In Progress", "/sessions/[id] (candidate working)",
|
||||
shell("Frontend pairing — Aisha Khan", "Session · In progress",
|
||||
page_content(detail_card(dict(title="Frontend pairing — Aisha Khan", code="4HTR-B2LN", expires="2026-10-17T16:30:00Z"), "In progress", ["React state bug (CODE)", "Event loop quiz (MCQ)"]))))
|
||||
add_screen(A, sid, "Session Detail Loading", "/sessions/[id] (loading)",
|
||||
shell("Session", "Loading", page_content(inst("Spin", "Spinner"))))
|
||||
err = row("Alert Error", ic("x-circle", 16, "var(--color-error)") + T("Message", "Load failed", 14) +
|
||||
box("Spacer", "", flex_grow="1") + inst("Ant Button / Default Small", "Retry", Label="Retry"), gap="8px", padding="8px 16px",
|
||||
background_color="var(--color-tag-red-bg)", border="1px solid var(--color-tag-red-bd)", border_radius="var(--radius-md)", width="100%")
|
||||
add_screen(A, sid, "Session Detail Error", "/sessions/[id] (error)", shell("Session", "Could not load session", page_content(err)))
|
||||
|
||||
|
||||
def build(A):
|
||||
sid = section(A, "Section / Admin / Sessions", "Admin / Sessions")
|
||||
list_screens(A, sid)
|
||||
new_screens(A, sid)
|
||||
detail_screens(A, sid)
|
||||
return sid
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fid, A = setup()
|
||||
print(build(A))
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Screenshot Admin screens: python3 adm_shots.py <section name> [screen name substr ...]"""
|
||||
import sys; sys.path.insert(0, "/Volumes/Storage/Projects/papercraft/examples/interview")
|
||||
from adm_common import *
|
||||
OUT = "/private/tmp/claude-501/-Volumes-Storage-Projects-photocraft/11f4040a-b4f4-4b9e-965a-bde08b400450/scratchpad/"
|
||||
fid, A = setup()
|
||||
sec = next(c for c in A.c("get_children", nodeId="root")["children"] if c["name"] == sys.argv[1])
|
||||
for slot in A.c("get_children", nodeId=sec["id"])["children"]:
|
||||
if not slot["name"].startswith("Screen Slot"): continue
|
||||
n = slot["name"][len("Screen Slot / "):]
|
||||
if len(sys.argv) > 2 and not any(a.lower() in n.lower() for a in sys.argv[2:]): continue
|
||||
scr = next(c for c in A.c("get_children", nodeId=slot["id"])["children"] if c["name"].startswith("Screen /"))
|
||||
p = OUT + "adm_" + n.replace(" ", "_") + ".png"
|
||||
tmp = A.artboard("TMP shot", "", width="1440px", height="900px", left="-3000px", top="0px")
|
||||
A.c("write_html", targetNodeId=tmp, mode="insert-children", html=f'<x-paper-clone node-id="{scr["id"]}"/>')
|
||||
A.screenshot(p, tmp, 1); print(p, scr["width"], scr["height"])
|
||||
A.c("delete_nodes", nodeIds=[tmp])
|
||||
r = A.lint(scr["id"])
|
||||
iss = r.get("issues") or r.get("findings") or r
|
||||
errs = [i for i in (iss if isinstance(iss, list) else []) if i.get("severity") in ("error", "warning")]
|
||||
print(" lint", len(errs), {(i.get("rule") or i.get("code")) for i in errs})
|
||||
@@ -0,0 +1,171 @@
|
||||
"""Helpers shared by admin_challenges.py and admin_bank.py (Admin page, Interview project)."""
|
||||
import json
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/Volumes/Storage/Projects/papercraft/examples/interview")
|
||||
from ds_common import * # noqa: F401,F403,E402
|
||||
|
||||
DS_ID = None
|
||||
|
||||
|
||||
def fg(c):
|
||||
return f"var(--color-{c})"
|
||||
|
||||
|
||||
def I(component, name=None, style=None, **ov):
|
||||
"""Instance html with overrides and optional root style (dict or str)."""
|
||||
o = json.dumps(ov, ensure_ascii=False).replace("'", "'")
|
||||
n = f' layer-name="{name}"' if name else ""
|
||||
st = ""
|
||||
if style:
|
||||
st = " style=\"" + (style if isinstance(style, str) else ";".join(f"{k.replace('_','-')}:{v}" for k, v in style.items())) + "\""
|
||||
return f"<x-paper-instance component=\"{component}\"{n}{st} overrides='{o}'/>"
|
||||
|
||||
|
||||
def init():
|
||||
fid = next(i["id"] for i in call("list_resources")["items"] if i["name"] == PROJECT)
|
||||
ps = pages(fid)
|
||||
call("import_components", {"fromPageId": ps["Design System"].id, "pageId": ps["Admin"].id})
|
||||
return fid, ps["Admin"]
|
||||
|
||||
|
||||
def delete_section(P, name):
|
||||
ids = [c["id"] for c in P.c("get_children", nodeId="root")["children"] if c["name"] == name]
|
||||
if ids:
|
||||
P.c("delete_nodes", nodeIds=ids)
|
||||
|
||||
|
||||
def write(P, parent, html):
|
||||
r = P.c("write_html", targetNodeId=parent, mode="insert-children", html=html)
|
||||
if r.get("ignoredStyles"):
|
||||
print(" ignored:", sorted(set(r["ignoredStyles"]))[:8])
|
||||
for w in r.get("warnings", []):
|
||||
print(" warn:", w)
|
||||
return r["createdNodeIds"]
|
||||
|
||||
|
||||
def ensure_components(P):
|
||||
"""Local main components (name `Group / Variant`): created on first run, kept afterwards."""
|
||||
have = {c["name"] for c in P.c("get_components")["components"]}
|
||||
specs = []
|
||||
toast = lambda nm, ic_, col_, bg, bd: row(nm, ic(ic_, 16, col_, "Icon") + T("Text", "Saved", 14, fg("text-primary")), gap="8px",
|
||||
padding="10px 16px", background=fg("bg-elevated"), border_radius="var(--radius-md)",
|
||||
box_shadow="var(--shadow-md)")
|
||||
specs.append(("Message / Success", toast("Message / Success", "check-circle", fg("success"), "", "")))
|
||||
specs.append(("Message / Error", toast("Message / Error", "x-circle", fg("error"), "", "")))
|
||||
hdr = row("Admin Shell Header / With Action",
|
||||
col("Titles", T("Title", "Challenges", 24, fg("text-primary"), 600, lh=32) + T("Subtitle", "Subtitle", 14, fg("text-secondary"))) +
|
||||
row("Actions", I("Button / Notification Bell") + I("Button / Primary", Label="New challenge"), gap="12px"),
|
||||
justify_content="space-between", width="1192px", padding="var(--page-padding)", background=fg("bg-elevated"),
|
||||
border="1px solid var(--color-border-light)")
|
||||
specs.append(("Admin Shell Header / With Action", hdr))
|
||||
for nm, h in specs:
|
||||
if nm in have:
|
||||
continue
|
||||
ids = write(P, "root", h)
|
||||
P.c("create_component", nodeId=ids[0], name=nm)
|
||||
P.c("update_styles", updates=[{"nodeIds": [ids[0]], "styles": {"left": 70000 + 400 * len(have), "top": 0}}])
|
||||
have.add(nm)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- antd-like pieces
|
||||
def label(txt, required=False):
|
||||
return row("Label", (T("Required Mark", "*", 14, fg("error")) if required else "") + T("Label Text", txt, 14, fg("text-primary")), gap="4px")
|
||||
|
||||
|
||||
def field(name, lab, ctrl, required=False, help_=None, err=None, width=None):
|
||||
kids = label(lab, required) + ctrl
|
||||
if err:
|
||||
kids += T("Error Message", err, 14, fg("error"))
|
||||
elif help_:
|
||||
kids += T("Help", help_, 12, fg("text-secondary"), lh=16)
|
||||
return col(name, kids, gap="8px", **({"width": width} if width else {}))
|
||||
|
||||
|
||||
def acard(name, title, body, extra_head=""):
|
||||
"""antd Card size=small with a title bar."""
|
||||
return col(name, row("Card Header", T("Card Title", title, 14, fg("text-primary"), 600), padding="0px 12px", height="38px", background=fg("bg-elevated")) +
|
||||
box("Divider", height="1px", background=fg("border-light")) +
|
||||
col("Card Body", body, padding="12px", gap="16px"),
|
||||
background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)", overflow="hidden")
|
||||
|
||||
|
||||
def pcard(name, kids, **st):
|
||||
"""packages/ui Card / antd Card default (24 padding)."""
|
||||
return col(name, kids, padding="var(--spacing-lg)", background=fg("bg-elevated"), border="1px solid var(--color-border-light)",
|
||||
border_radius="var(--radius-lg)", **st)
|
||||
|
||||
|
||||
def dashed_btn(label_, name="Button / Dashed", block=False, small=False):
|
||||
h = 24 if small else 32
|
||||
return row(name, ic("plus", 12, fg("text-primary"), "Plus") + T("Label", label_.lstrip("+ "), 14, fg("text-primary")), gap="4px", justify_content="center",
|
||||
height=f"{h}px", padding="0px 12px" if small else "0px 16px", border="1px dashed var(--color-border)", border_radius="var(--radius-md)",
|
||||
background=fg("bg-elevated"), **({} if block else {"width": "fit-content"}))
|
||||
|
||||
|
||||
def x_btn(name="Remove"):
|
||||
return row(name, ic("x", 14, fg("text-secondary"), "Close"), justify_content="center", width="32px", height="32px", border_radius="var(--radius-md)")
|
||||
|
||||
|
||||
def circle_letter(txt, on=False, size=24):
|
||||
return row("Letter", T("Letter Text", txt, 12, fg("text-inverse") if on else fg("text-secondary"), 600, lh=16), justify_content="center",
|
||||
width=f"{size}px", height=f"{size}px", border_radius="99px", background=fg("success") if on else "#f0f0f0")
|
||||
|
||||
|
||||
def md_editor(text_lines, height=320):
|
||||
tb = row("Toolbar", "".join(ic(n, 14, fg("text-disabled"), n) for n in ["text-b", "text-italic", "text-strikethrough", "text-h", "link", "quotes", "code", "image", "list-bullets", "list-numbers", "question"]),
|
||||
gap="16px", padding="0px 12px", height="32px", background=fg("code-bg"), border_bottom="1px solid #ffffff1f")
|
||||
body = col("Source", "".join(T("Line", l or " ", 12, fg("code-fg"), 400, lh=16) for l in text_lines), gap="0px", padding="12px", flex_grow="1")
|
||||
return col("Markdown Editor", tb + body, height=f"{height}px", background=fg("code-bg"), border_radius="var(--radius-md)", overflow="hidden")
|
||||
|
||||
|
||||
def code_block(path, lines, height=None):
|
||||
return col("Code Preview", T("File Path", path, 12, fg("text-secondary"), lh=16, padding="6px 12px", background=fg("bg-sider"), border_bottom="1px solid var(--color-border-light)") +
|
||||
col("Code", "".join(T("Line", l or " ", 12, fg("code-fg"), 400, lh=18) for l in lines), padding="12px", background=fg("code-bg"), gap="0px",
|
||||
**({"height": f"{height}px"} if height else {})),
|
||||
border="1px solid var(--color-border-light)", border_radius="var(--radius-md)", overflow="hidden")
|
||||
|
||||
|
||||
TYPE_TAG = {"Algo": "Blue", "Fullstack": "Purple", "MCQ": "Cyan", "SQL": "Gold", "Theory": "Green"}
|
||||
|
||||
|
||||
def type_tag(t):
|
||||
return I(f"Tag / {TYPE_TAG[t]}", Label=t)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- shell
|
||||
def screen(name, route, active, title, subtitle, content, actions=False, action_label="New challenge", height=900, overlay=None, toast=None, content_gap=0):
|
||||
"""Screen frame 1440 wide: dark sidebar + header + content. Returns (caption + screen) html."""
|
||||
sb = col("Sidebar", I(f"Admin Sidebar / {active} Active", style="flex-shrink:0"), width="248px", background=fg("sidebar-bg"))
|
||||
if actions:
|
||||
hd = I("Admin Shell Header / With Action", Title=title, Subtitle=subtitle, **{"Label": action_label})
|
||||
else:
|
||||
hd = I("Admin Shell Header", Title=title, Subtitle=subtitle)
|
||||
main = col("Main", hd + row("Content Area", col("Content", content, width="1100px", padding="var(--page-padding)", gap=f"{content_gap}px"),
|
||||
justify_content="center", align_items="flex-start"), width="1192px")
|
||||
extra = overlay or ""
|
||||
if toast:
|
||||
extra += row("Toast Layer", toast, justify_content="center", position="absolute", left="248px", top="16px", width="1192px")
|
||||
return row(f"Screen / {name}", sb + main + extra, width="1440px", background=fg("bg-layout"), position="relative", overflow="hidden" if height == 900 else "visible", height=("900px" if height == 900 else "fit-content"))
|
||||
|
||||
|
||||
def cap(route, extra=""):
|
||||
return T("Route Caption", route + (f" {extra}" if extra else ""), 12, fg("text-secondary"), lh=16)
|
||||
|
||||
|
||||
def shell(name, route, *a, caption_extra="", **kw):
|
||||
return col(f"Frame / {name}", cap(route, caption_extra) + screen(name, route, *a, **kw), gap="8px")
|
||||
|
||||
|
||||
def section(name, title, items, x, y, cols=1):
|
||||
"""items: html strings (each a `Frame / ...`). Arranged in rows of `cols`."""
|
||||
rows_ = ""
|
||||
for i in range(0, len(items), cols):
|
||||
rows_ += row(f"Row {i // cols + 1}", "".join(items[i:i + cols]), gap="var(--spacing-xl)", align_items="flex-start")
|
||||
return col(name, T("Section Title", title, 24, fg("text-primary"), 700, lh=32) + col("Screens", rows_, gap="var(--spacing-xl)"),
|
||||
padding="var(--spacing-xl)", gap="var(--spacing-xl)", background="transparent", position="absolute", left=f"{x}px", top=f"{y}px")
|
||||
|
||||
|
||||
def overlay_html(inner, w=1440, h=900, name="Overlay"):
|
||||
return row(name, inner, justify_content="center", align_items="center", position="absolute", left="0px", top="0px", right="0px", bottom="0px",
|
||||
background=fg("bg-overlay"))
|
||||
@@ -0,0 +1,16 @@
|
||||
"""python3 admin_ch_shot.py <Section name> [screen-substring] -> PNGs in scratchpad."""
|
||||
import sys
|
||||
sys.path.insert(0, "/Volumes/Storage/Projects/papercraft/examples/interview")
|
||||
from admin_ch_common import *
|
||||
OUT = "/private/tmp/claude-501/-Volumes-Storage-Projects-photocraft/11f4040a-b4f4-4b9e-965a-bde08b400450/scratchpad/"
|
||||
fid, P = init()
|
||||
sec = next(c for c in P.c("get_children", nodeId="root")["children"] if c["name"] == sys.argv[1])
|
||||
def walk(n, d=0):
|
||||
for c in P.c("get_children", nodeId=n)["children"]:
|
||||
yield c
|
||||
if d < 3 and c["name"].startswith(("Screens","Row ","Frame /")): yield from walk(c["id"], d+1)
|
||||
want = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
for c in walk(sec["id"]):
|
||||
if c["name"].startswith("Screen / ") and want in c["name"]:
|
||||
print(c["name"], c["width"], c["height"])
|
||||
P.screenshot(OUT + c["name"].replace(" / ", "_").replace(" ", "_") + ".png", c["id"], 1)
|
||||
@@ -0,0 +1,362 @@
|
||||
"""Admin / Challenges screens (idempotent): list states, new/edit challenge editors, templates modal.
|
||||
|
||||
python3 admin_challenges.py [only-screen-substring]
|
||||
"""
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/Volumes/Storage/Projects/papercraft/examples/interview")
|
||||
from admin_ch_common import * # noqa: F401,F403
|
||||
|
||||
SECTION = "Section / Admin / Challenges"
|
||||
X0, Y0 = 0, 12000
|
||||
|
||||
ROWS = [
|
||||
("Two Sum", "Algo", False), ("Valid Parentheses", "Algo", False), ("REST Todo API", "Fullstack", False),
|
||||
("JavaScript Closures", "MCQ", False), ("Second Highest Salary", "SQL", False),
|
||||
("System Design: URL Shortener", "Theory", False), ("Behavioral: Conflict", "Theory", True), ("Async / Await", "MCQ", True),
|
||||
]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ list page
|
||||
def filter_bar(active="All types", query=None, archived=False):
|
||||
types = ["All types", "Algo", "Fullstack", "MCQ", "SQL", "Theory"]
|
||||
pills = "".join(I("Pill / Active" if t == active else "Pill / Inactive", Label=t) for t in types)
|
||||
search = I("Search Input / Filled" if query else "Search Input", Placeholder=query or "Search challenges…", style="width:240px")
|
||||
seg = I("Segmented / 2 Options Active 2" if archived else "Segmented / 2 Options Active 1", **{"Option 1": "Active", "Option 2": "All"})
|
||||
return row("Filter Bar", row("Type Pills", pills, gap="8px") + row("Search And Scope", search + seg, gap="8px"),
|
||||
justify_content="space-between", gap="var(--spacing-md)")
|
||||
|
||||
|
||||
def table(rows_):
|
||||
def th(t, w=None, right=False):
|
||||
return T(f"Col / {t}", t.upper(), 12, fg("text-secondary"), 600, lh=16, letter_spacing="0.36px",
|
||||
**({"width": f"{w}px"} if w else {"flex_grow": "1"}), **({"text_align": "right"} if right else {}))
|
||||
head = row("Header Row", th("Title") + th("Type", 140) + th("Status", 130) + th("Actions", 170, True), gap="16px", padding="12px 16px",
|
||||
background=fg("bg-sider"), border_bottom="1px solid var(--color-border-light)")
|
||||
body = ""
|
||||
for title, t, arch in rows_:
|
||||
status = I("Tag Dot / Gray", Label="Archived") if arch else I("Tag Dot / Green", Label="Active")
|
||||
acts = I("Button / Default Small", Label="Edit") + ("" if arch else I("Button / Danger Small", Label="Archive"))
|
||||
body += row(f"Row / {title}", T("Cell / Title", title, 14, fg("text-primary"), 600, flex_grow="1") +
|
||||
row("Cell / Type", type_tag(t), width="140px") + row("Cell / Status", status, width="130px") +
|
||||
row("Cell / Actions", acts, width="170px", gap="8px", justify_content="flex-end"),
|
||||
gap="16px", padding="12px 16px", border_bottom="1px solid var(--color-border-light)", background=fg("bg-elevated"))
|
||||
return col("Data Table", head + body, background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)", overflow="hidden")
|
||||
|
||||
|
||||
def list_screen(name, route, content, **kw):
|
||||
return shell(name, route, "Challenges", "Challenges", "Reusable interview prompts across roles", content, actions=True, **kw)
|
||||
|
||||
|
||||
def empty_block(title="No challenges match", desc="Adjust the filters or create a new challenge to get started."):
|
||||
return I("Empty State", Title=title, Description=desc, style="width:1052px") if False else col(
|
||||
"Empty State Block", I("Empty State", Title=title, Description=desc) + I("Button / Primary", Label="New challenge"), align_items="center", gap="8px",
|
||||
padding="var(--spacing-xl)", width="1052px")
|
||||
|
||||
|
||||
def loading_block():
|
||||
return row("Loading", I("Spin / Large"), justify_content="center", padding="var(--spacing-xxl)", width="1052px")
|
||||
|
||||
|
||||
def list_screens():
|
||||
out = []
|
||||
bar = lambda **k: filter_bar(**k)
|
||||
out.append(list_screen("Challenges List", "/challenges", bar() + table(ROWS[:7][:6] + [ROWS[7]][:0]), caption_extra="populated, active only") if False else
|
||||
list_screen("Challenges List", "/challenges", bar() + table(ROWS[:6]), caption_extra="populated (Active)"))
|
||||
out.append(list_screen("Challenges List Filtered", "/challenges", bar(active="MCQ", query="clos") + table([ROWS[3]]), caption_extra="type pill = MCQ, search 'clos'"))
|
||||
out.append(list_screen("Challenges List With Archived", "/challenges", bar(archived=True) + table(ROWS), caption_extra="scope = All, archived rows have no Archive button"))
|
||||
out.append(list_screen("Challenges List Empty", "/challenges", bar(active="SQL", query="zzz") + empty_block(), caption_extra="no match"))
|
||||
out.append(list_screen("Challenges List Loading", "/challenges", bar() + loading_block(), caption_extra="loading"))
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ editor pieces
|
||||
MD_MCQ = ["## JavaScript Closures", "", "What does the following code log?", "", "```ts", "function makeCounter() {", " let n = 0;", " return () => ++n;", "}",
|
||||
"const c = makeCounter();", "console.log(c(), c(), c());", "```"]
|
||||
|
||||
|
||||
def sel(value, w=None, disabled=False, ph=False):
|
||||
comp = "Select / Filled" if value else "Select"
|
||||
st = {"width": f"{w}px"} if w else {}
|
||||
if disabled:
|
||||
st["opacity"] = ".5"
|
||||
return I(comp, Value=value or "Select…", style=st or None)
|
||||
|
||||
|
||||
def inp(value, w=None, err=False):
|
||||
st = f"width:{w}px" if w else None
|
||||
return I("Input / Error" if err else ("Input / Filled" if value else "Input"), Value=value or "", style=st)
|
||||
|
||||
|
||||
def type_details(type_label, title, tmo=5, mem=256, edit=False, title_err=False):
|
||||
ctrl_w = "100%"
|
||||
return acard("Card / Type & details", "Type & details",
|
||||
field("Field / Type", "Type", sel(type_label, 1004 - 24 - 0 if False else 560, disabled=edit), True) +
|
||||
field("Field / Title", "Title", inp(title, 560, err=title_err), True, err="Please enter Title" if title_err else None) +
|
||||
row("Limits", field("Field / Time limit", "Time limit (s)", I("Input Number", Value=str(tmo), style="width:120px"), True) +
|
||||
field("Field / Memory", "Memory (MB)", I("Input Number", Value=str(mem), style="width:120px"), True), gap="16px", align_items="flex-start"))
|
||||
|
||||
|
||||
def prompt_card(lines, err=False):
|
||||
return acard("Card / Prompt", "Prompt", field("Field / Prompt", "Prompt (Markdown)", md_editor(lines, 240), True))
|
||||
|
||||
|
||||
def lang_card(lang="TypeScript", err=False):
|
||||
return acard("Card / Language & limits", "Language & limits",
|
||||
field("Field / Language", "Language", sel(lang or "", 560) if lang else I("Select", Value="Select…", style="width:560px") if not err else I("Select", Value="Select…", style="width:560px"), True,
|
||||
err="Please enter Language" if err else None))
|
||||
|
||||
|
||||
def switch(on, txt):
|
||||
return row("Switch / " + txt, T("Switch Text", txt, 12, fg("text-inverse") if on else fg("text-inverse"), 400, lh=16), padding="0px 8px", height="22px", width="64px",
|
||||
justify_content="flex-end" if not on else "flex-start", border_radius="99px", background=fg("brand-primary") if on else fg("switch-off"))
|
||||
|
||||
|
||||
def files_card(files):
|
||||
body = ""
|
||||
for path, contents, hidden in files:
|
||||
body += row(f"File Row / {path}", inp(path, 200) if False else I("Input / Filled", Value=path, style="width:200px") + I("Input / Filled", Value=contents, style="flex-grow:1") +
|
||||
switch(hidden, "hidden" if hidden else "visible") + x_btn(), gap="8px")
|
||||
return acard("Card / Files", "Files", body + dashed_btn("+ Add file", "Button / Add file"))
|
||||
|
||||
|
||||
def tests_card(tests):
|
||||
body = ""
|
||||
for n, inp_, exp, w, samp in tests:
|
||||
body += row(f"Test Row / {n}", I("Input / Filled", Value=n, style="width:120px") + I("Input / Filled", Value=inp_, style="flex-grow:1") +
|
||||
I("Input / Filled", Value=exp, style="flex-grow:1") + I("Input Number", Value=str(w), style="width:70px") +
|
||||
switch(samp, "sample" if samp else "-") + x_btn(), gap="8px")
|
||||
return acard("Card / Test cases", "Test cases", body + dashed_btn("+ Add test", "Button / Add test"))
|
||||
|
||||
|
||||
def mcq_card(questions, warn=None, hint=True):
|
||||
body = T("Help", "Add as many questions as you need — each has its own options and exactly one correct answer. With a single question you can leave its text empty and let the prompt above do the talking.",
|
||||
12, fg("text-secondary"), lh=18)
|
||||
qs = ""
|
||||
for qi, (qtext, opts) in enumerate(questions):
|
||||
orow = ""
|
||||
for oi, (lab, ok) in enumerate(opts):
|
||||
L = "ABCDEFG"[oi]
|
||||
orow += row(f"Option Row / {L}", circle_letter(L, ok) + (T("Option Text", lab, 14, fg("text-primary"), flex_grow="1") if lab else T("Option Placeholder", f"Option {L}", 14, fg("text-placeholder"), flex_grow="1")) +
|
||||
I("Checkbox / Checked" if ok else "Checkbox / Unchecked", Label="Correct") + x_btn(), gap="8px", padding="4px 8px",
|
||||
border=f"1px solid {'var(--color-success)' if ok else 'var(--color-border)'}", border_radius="var(--radius-md)",
|
||||
background="#f6ffed" if ok else "transparent")
|
||||
w = T("Warning", warn, 14, fg("warning")) if (warn and qi == 0) else ""
|
||||
qs += col(f"Question {qi + 1}", row("Question Head", T("Question Label", f"Question {qi + 1}", 14, fg("text-primary"), 600) +
|
||||
(I("Input / Filled", Value=qtext, style="flex-grow:1") if qtext else I("Input", Value="Question text (required)", style="flex-grow:1")) + x_btn(), gap="8px") +
|
||||
orow + w + dashed_btn("+ Add option", "Button / Add option", small=True), gap="8px", padding="8px 16px",
|
||||
border="1px solid var(--color-border)", border_radius="var(--radius-md)")
|
||||
return acard("Card / MCQ questions", "MCQ questions", body + qs + dashed_btn("+ Add question", "Button / Add question", block=True))
|
||||
|
||||
|
||||
def theory_card(qs, hint=False):
|
||||
body = ""
|
||||
for i, (cat, diff, q) in enumerate(qs, 1):
|
||||
body += row(f"Theory Row {i}", circle_letter(str(i), False) +
|
||||
col("Fields", row("Cat And Diff", (T("Category", cat, 14, fg("text-primary"), flex_grow="1") if cat else T("Category Placeholder", "Category (e.g. JS Closures)", 14, fg("text-placeholder"), flex_grow="1")) +
|
||||
row("Diff Select", T("Diff", diff, 14, fg("text-primary")) + ic("caret-down", 12, fg("text-disabled"), "Caret"), gap="8px", width="110px", justify_content="space-between"),
|
||||
gap="8px") +
|
||||
(T("Question", q, 14, fg("text-primary")) if q else T("Question Placeholder", "Question the recruiter asks", 14, fg("text-placeholder"))), gap="4px", flex_grow="1") + x_btn(),
|
||||
gap="8px", padding="4px 8px", border="1px solid var(--color-border)", border_radius="var(--radius-md)", align_items="flex-start")
|
||||
if not qs:
|
||||
body += ""
|
||||
body += dashed_btn("+ Add question", "Button / Add question", block=True)
|
||||
if not qs:
|
||||
body += T("Hint", "Add at least one question — theory challenges require ≥1.", 14, fg("text-secondary"))
|
||||
return acard("Card / Theory questions", "Theory questions", body)
|
||||
|
||||
|
||||
def submit(label_, disabled=False):
|
||||
return I("Ant Button / Primary", Label=label_, style="opacity:.5" if disabled else None)
|
||||
|
||||
|
||||
def preview(title, prompt_nodes, body_nodes):
|
||||
def sec(t):
|
||||
return T("Section Label", t.upper(), 12, fg("text-secondary"), 600, lh=16, letter_spacing="0.48px")
|
||||
hint = lambda t: T("Empty Hint", t, 12, fg("text-disabled"), lh=16, font_style="italic")
|
||||
return col("Live Preview", T("Preview Label", "Live preview", 12, fg("text-primary"), lh=16) +
|
||||
T("Preview Title", title or "Untitled challenge", 20, fg("text-primary"), 600, lh=28) +
|
||||
col("Prompt Block", sec("Prompt") + prompt_nodes, gap="8px") + body_nodes,
|
||||
padding="var(--spacing-lg)", gap="var(--spacing-lg)", background=fg("bg-elevated"), border="1px solid var(--color-border-light)",
|
||||
border_radius="var(--radius-lg)")
|
||||
|
||||
|
||||
def sec_label(t):
|
||||
return T("Section Label", t.upper(), 12, fg("text-secondary"), 600, lh=16, letter_spacing="0.48px")
|
||||
|
||||
|
||||
def hint(t):
|
||||
return T("Empty Hint", t, 12, fg("text-disabled"), lh=16)
|
||||
|
||||
|
||||
def md_rendered(h, para, code=None):
|
||||
kids = T("Heading", h, 24, fg("text-primary"), 600, lh=32) + box("Rule", height="1px", background=fg("border-light")) + T("Paragraph", para, 14, fg("text-primary"))
|
||||
if code:
|
||||
kids += col("Code Block", "".join(T("Line", l or " ", 12, fg("text-primary"), lh=16) for l in code), padding="12px", background=fg("bg-layout"), border_radius="var(--radius-md)")
|
||||
return col("Rendered Markdown", kids, gap="12px")
|
||||
|
||||
|
||||
def opt_preview(opts):
|
||||
return col("Options", "".join(row(f"Option / {i}", box("Radio", width="16px", height="16px", border_radius="99px", border="2px solid var(--color-border-strong)") +
|
||||
T("Label", o, 14, fg("text-primary"), flex_grow="1"), gap="8px", padding="10px 12px",
|
||||
border="1px solid var(--color-border-light)", border_radius="var(--radius-md)") for i, o in enumerate(opts)), gap="8px")
|
||||
|
||||
|
||||
def two_pane(form_html, preview_html):
|
||||
return row("Two Pane", col("Form", form_html, gap="var(--spacing-lg)", width="615px") + col("Preview Pane", preview_html, width="385px"),
|
||||
gap="var(--spacing-lg)", align_items="flex-start")
|
||||
|
||||
|
||||
def editor_page(title, subtitle, inner):
|
||||
return pcard("Card / Editor", T("Card Heading", title, 24, fg("text-primary"), 600, lh=32) + inner, gap="var(--spacing-md)", width="1052px")
|
||||
|
||||
|
||||
def template_picker(applied=None):
|
||||
kids = T("Label", "Starter templates", 14, fg("text-primary"), 600) + I("Ant Button / Default", Label="Browse templates…") + I("Ant Button / Text", Label="Start blank")
|
||||
if applied:
|
||||
kids += I("Tag / Blue", Label=f"{applied} applied")
|
||||
kids += T("Help", "Templates pre-fill the form — everything stays editable.", 12, fg("text-secondary"), lh=16)
|
||||
return row("Template Picker", kids, gap="8px", flex_wrap="wrap")
|
||||
|
||||
|
||||
def algo_form(title, with_files, errs=False):
|
||||
files = files_card([("solution.ts", "export function twoSum(nums: number[], target: number): number[] {…}", False)] if with_files else [])
|
||||
tests = tests_card([("basic", "[2,7,11,15], 9", "[0,1]", 1, True), ("negatives", "[-3,4,3,90], 0", "[0,2]", 2, False), ("no pair", "[1,2], 7", "[]", 1, False)] if with_files else [])
|
||||
return type_details("Algo", title, title_err=errs) + prompt_card(MD_MCQ[:1] and ["## Two Sum", "", "Given an array of integers nums and an integer target,", "return the indices of the two numbers that add up to target.", "", "```ts", "twoSum([2, 7, 11, 15], 9); // [0, 1]", "```"] if with_files else [""]) + \
|
||||
lang_card("TypeScript" if with_files else None, err=errs) + files + tests + submit("Create")
|
||||
|
||||
|
||||
def build_new(errs=False, applied=None, filled=True):
|
||||
pv_prompt = md_rendered("Two Sum", "Given an array of integers nums and an integer target, return the indices of the two numbers that add up to target.") if filled else hint("Add a prompt to see it rendered here.")
|
||||
pv_body = col("Body", sec_label("Starter file") + (code_block("solution.ts", ["export function twoSum(nums: number[], target: number): number[] {", " // TODO", " return [];", "}"]) if filled else hint("No visible files yet — add one in the Files section.")) +
|
||||
sec_label("Test cases") + (T("Tests", "Tests: basic, negatives, no pair", 14, fg("text-primary")) if filled else hint("No test cases yet.")), gap="8px")
|
||||
form = algo_form("Two Sum" if filled else "", filled, errs)
|
||||
return editor_page("New challenge", "", template_picker(applied) + two_pane(form, preview("Two Sum" if filled else "", pv_prompt, pv_body)))
|
||||
|
||||
|
||||
def build_edit_mcq():
|
||||
form = (type_details("MCQ", "JavaScript Closures", edit=True) + prompt_card(MD_MCQ) + files_card([]) +
|
||||
mcq_card([("What does the code log when run once?", [("1 1 1", False), ("1 2 3", True), ("0 1 2", False), ("3 2 1", False)]),
|
||||
("Which keyword preserves n?", [("var", False), ("let", False)])], warn="Mark one option as correct."
|
||||
if False else None) + submit("Save"))
|
||||
pv = preview("JavaScript Closures", md_rendered("JavaScript Closures", "What does the following code log?", MD_MCQ[6:11]),
|
||||
col("Questions", sec_label("Question 1") + T("Q1", "What does the code log when run once?", 14, fg("text-primary")) + opt_preview(["1 1 1", "1 2 3", "0 1 2", "3 2 1"]) +
|
||||
sec_label("Question 2") + T("Q2", "Which keyword preserves n?", 14, fg("text-primary")) + opt_preview(["var", "let"]), gap="8px"))
|
||||
return editor_page("Edit challenge", "", two_pane(form, pv))
|
||||
|
||||
|
||||
def build_edit_mcq_validation():
|
||||
form = (type_details("MCQ", "", edit=True, title_err=True) + prompt_card(["## JavaScript Closures"]) + files_card([]) +
|
||||
mcq_card([("", [("1 1 1", False), ("1 2 3", False), ("", False)])], warn="Mark one option as correct.") + submit("Save"))
|
||||
pv = preview("Untitled challenge", md_rendered("JavaScript Closures", ""), col("Questions", sec_label("Options") + opt_preview(["1 1 1", "1 2 3", "Option 3"]), gap="8px"))
|
||||
return editor_page("Edit challenge", "", two_pane(form, pv))
|
||||
|
||||
|
||||
def build_edit_theory(empty=False):
|
||||
qs = [] if empty else [("System Design", "Medium", "How would you design a URL shortener that handles 10k writes per second?"),
|
||||
("Databases", "Hard", "Explain how you would shard the mapping table."), ("", "Easy", "")]
|
||||
form = (type_details("Theory", "System Design: URL Shortener", edit=True) + prompt_card(["## System Design", "", "Discuss the trade-offs aloud."]) + files_card([]) +
|
||||
theory_card(qs) + submit("Save", disabled=empty))
|
||||
if empty:
|
||||
tp = hint("No questions yet — add at least one to make this a valid theory challenge.")
|
||||
else:
|
||||
def tq(i, cat, d, q, c):
|
||||
return row(f"Question {i}", T("Num", f"{i}.", 14, fg("text-secondary"), 700) + col("Body", row("Meta", T("Category", cat, 12, fg("text-secondary"), 600, lh=16) +
|
||||
row("Diff", T("Diff Text", d, 12, c, 600, lh=16), padding="1px 7px", border_radius="99px", background="#f0f0f0"), gap="6px") + T("Text", q, 14, fg("text-primary")), gap="4px", flex_grow="1"),
|
||||
gap="8px", padding="10px 12px", border="1px solid var(--color-border-light)", border_radius="var(--radius-md)")
|
||||
tp = col("Qs", tq(1, "System Design", "Medium", qs[0][2], fg("warning")) + tq(2, "Databases", "Hard", qs[1][2], fg("error")), gap="8px")
|
||||
pv = preview("System Design: URL Shortener", md_rendered("System Design", "Discuss the trade-offs aloud."), col("Questions", sec_label("Questions") + tp, gap="8px"))
|
||||
return editor_page("Edit challenge", "", two_pane(form, pv))
|
||||
|
||||
|
||||
def build_edit_sql():
|
||||
sql = ["SELECT department, SUM(amount) AS revenue", "FROM sales", "GROUP BY department", "ORDER BY revenue DESC", "LIMIT :n;"]
|
||||
form = (type_details("SQL", "Top-N by Revenue", edit=True) + prompt_card(["## Top-N by Revenue", "", "Write a query returning the top N departments by revenue."]) +
|
||||
files_card([("solution.sql", "SELECT department, SUM(amount) …", False), ("schema.sql", "CREATE TABLE sales (…)", True)]) + submit("Save"))
|
||||
pv = preview("Top-N by Revenue", md_rendered("Top-N by Revenue", "Write a query returning the top N departments by revenue."),
|
||||
col("SQL", sec_label("SQL") + code_block("solution.sql", sql), gap="8px"))
|
||||
return editor_page("Edit challenge", "", two_pane(form, pv))
|
||||
|
||||
|
||||
def build_edit_fullstack():
|
||||
code = ["import express from 'express';", "const app = express();", "app.get('/todos', (_req, res) => res.json([]));", "app.listen(3000);"]
|
||||
form = (type_details("Fullstack", "REST Todo API", tmo=30, mem=512, edit=True) + prompt_card(["## REST Todo API", "", "Implement GET /todos and POST /todos."]) +
|
||||
lang_card("TypeScript") + files_card([("server.ts", "import express from 'express';…", False), ("server.test.ts", "describe('todos', …)", True)]) +
|
||||
tests_card([("lists todos", "GET /todos", "200 []", 1, True), ("creates todo", "POST /todos", "201", 2, False)]) + submit("Save"))
|
||||
pv = preview("REST Todo API", md_rendered("REST Todo API", "Implement GET /todos and POST /todos."),
|
||||
col("Body", sec_label("Starter file") + code_block("server.ts", code) + sec_label("Test cases") + T("Tests", "Tests: lists todos, creates todo", 14, fg("text-primary")), gap="8px"))
|
||||
return editor_page("Edit challenge", "", two_pane(form, pv))
|
||||
|
||||
|
||||
def build_loading():
|
||||
return row("Loading", I("Spin / Large"), justify_content="center", padding="var(--spacing-xxl)", width="1052px")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ templates modal
|
||||
TEMPLATES = [
|
||||
("Algorithm", "Two Sum", "Classic array warm-up with a TS starter and 3 sample tests."),
|
||||
("Algorithm", "Valid Parentheses", "Stack-based bracket matcher. Empty starter + sample tests."),
|
||||
("Full-stack", "REST Todo API", "Express-style TS server stub with a sample happy-path test."),
|
||||
("Full-stack", "Counter Component", "React counter component starter. No tests — UI focus."),
|
||||
("Multiple choice", "JS Closures", "Four options, one correct — tests closure capture semantics."),
|
||||
("Multiple choice", "Async / Await", "Pick the correct ordering of awaited console logs."),
|
||||
("SQL", "Top-N by Revenue", "Schema note + prompt; candidate writes the ranking query."),
|
||||
("SQL", "Second Highest Salary", "Classic windowing exercise. Returns null when there is none."),
|
||||
("Theory", "System Design: URL Shortener", "Three questions across easy/medium/hard design depth."),
|
||||
]
|
||||
BADGE = {"Algorithm": ("brand-primary", "tag-blue-bg"), "Full-stack": ("avatar-purple", "tag-purple-bg"), "Multiple choice": ("avatar-teal", "tag-cyan-bg"),
|
||||
"SQL": ("warning", "tag-gold-bg"), "Theory": ("avatar-pink", "tag-red-bg")}
|
||||
|
||||
|
||||
def tcard(kind, title, blurb):
|
||||
fgc, bgc = BADGE[kind]
|
||||
return col(f"Template Card / {title}", row("Head", row("Type Badge", box("Dot", width="8px", height="8px", border_radius="99px", background=fg(fgc)) + T("Label", kind, 12, fg(fgc), 600, lh=16),
|
||||
gap="6px", padding="2px 8px", border_radius="99px", background=fg(bgc)), justify_content="space-between") +
|
||||
T("Title", title, 14, fg("text-primary"), 600) + T("Blurb", blurb, 12, fg("text-secondary"), lh=18),
|
||||
gap="4px", padding="var(--spacing-md)", background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)", box_shadow="var(--shadow-sm)", width="256px")
|
||||
|
||||
|
||||
def templates_modal():
|
||||
seg = I("Segmented / 4 Options Active 1", **{"Option 1": "All", "Option 2": "Algorithm", "Option 3": "Full-stack", "Option 4": "Multiple choice"}) if False else \
|
||||
row("Type Filter", "".join(T(f"Opt {o}", o, 14, fg("text-primary") if o == "All" else fg("text-secondary"), 600 if o == "All" else 400, padding="4px 12px",
|
||||
**({"background": fg("bg-elevated"), "border_radius": "6px"} if o == "All" else {})) for o in ["All", "Algorithm", "Full-stack", "Multiple choice", "SQL", "Theory"]),
|
||||
padding="2px", background="#0000000f", border_radius="var(--radius-md)")
|
||||
grid = ""
|
||||
for i in range(0, len(TEMPLATES), 3):
|
||||
grid += row(f"Grid Row {i // 3 + 1}", "".join(tcard(*t) for t in TEMPLATES[i:i + 3]), gap="var(--spacing-md)")
|
||||
m = col("Modal / Starter Templates", row("Modal Head", T("Title", "Starter templates", 16, fg("text-primary"), 600, lh=24) + ic("x", 16, fg("text-secondary"), "Close"), justify_content="space-between") +
|
||||
row("Toolbar", I("Input / Search", Value="Search templates", style="width:280px") + seg, gap="8px") + col("Grid", grid, gap="var(--spacing-md)"),
|
||||
width="860px", padding="20px 24px", gap="var(--spacing-md)", background=fg("bg-elevated"), border_radius="var(--radius-lg)", box_shadow="var(--shadow-lg)")
|
||||
return overlay_html(m)
|
||||
|
||||
|
||||
def build_all(P, only=None):
|
||||
items = []
|
||||
for f in list_screens():
|
||||
items.append(f)
|
||||
new_full = lambda **k: shell("Challenge New", "/challenges/new", "Challenges", "New challenge", "Author a reusable interview prompt", build_new(**k), wide=True) if False else None
|
||||
S = lambda name, route, title, sub, content, **kw: shell(name, route, "Challenges", title, sub, content, **{"height": 0, **kw})
|
||||
items += [
|
||||
S("Challenge New Blank", "/challenges/new", "New challenge", "Author a reusable interview prompt", build_new(filled=False), caption_extra="blank (Algo)"),
|
||||
S("Challenge New Template Applied", "/challenges/new", "New challenge", "Author a reusable interview prompt", build_new(applied="Two Sum"), caption_extra="template applied"),
|
||||
S("Challenge New Validation Errors", "/challenges/new", "New challenge", "Author a reusable interview prompt", build_new(errs=True, filled=False), caption_extra="submit with empty required fields"),
|
||||
S("Challenge New Templates Modal", "/challenges/new", "New challenge", "Author a reusable interview prompt", build_new(filled=False), overlay=templates_modal(), height=900, caption_extra="Browse templates…"),
|
||||
S("Challenge Edit Mcq", "/challenges/[id]/edit", "Edit challenge", "Update prompt, rubric & metadata", build_edit_mcq(), caption_extra="MCQ"),
|
||||
S("Challenge Edit Mcq Validation", "/challenges/[id]/edit", "Edit challenge", "Update prompt, rubric & metadata", build_edit_mcq_validation(), caption_extra="MCQ, no correct option, empty title"),
|
||||
S("Challenge Edit Theory", "/challenges/[id]/edit", "Edit challenge", "Update prompt, rubric & metadata", build_edit_theory(), caption_extra="Theory"),
|
||||
S("Challenge Edit Theory Empty", "/challenges/[id]/edit", "Edit challenge", "Update prompt, rubric & metadata", build_edit_theory(True), caption_extra="Theory without questions: Save disabled"),
|
||||
S("Challenge Edit Sql", "/challenges/[id]/edit", "Edit challenge", "Update prompt, rubric & metadata", build_edit_sql(), caption_extra="SQL"),
|
||||
S("Challenge Edit Fullstack", "/challenges/[id]/edit", "Edit challenge", "Update prompt, rubric & metadata", build_edit_fullstack(), caption_extra="Fullstack"),
|
||||
S("Challenge Edit Loading", "/challenges/[id]/edit", "Edit challenge", "Loading", build_loading(), height=900, caption_extra="loading"),
|
||||
]
|
||||
return items
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fid, P = init()
|
||||
ensure_components(P)
|
||||
delete_section(P, SECTION)
|
||||
items = build_all(P)
|
||||
sec = section(SECTION, "Admin / Challenges", items, X0, Y0, cols=2)
|
||||
ids = write(P, "root", sec)
|
||||
print("section", ids)
|
||||
@@ -0,0 +1,559 @@
|
||||
"""Admin app shell + Auth, Dashboard, Configuration, Criteria and Schedule screens (page `Admin`).
|
||||
|
||||
Idempotent: deletes its own `Section / Admin / <X>` frames and its own main components, then rebuilds.
|
||||
python3 admin_core.py [auth dashboard configuration criteria schedule] (default: all)
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/Volumes/Storage/Projects/papercraft/examples/interview")
|
||||
from ds_common import * # noqa: F401,F403,E402
|
||||
|
||||
OUT = "/private/tmp/claude-501/-Volumes-Storage-Projects-photocraft/11f4040a-b4f4-4b9e-965a-bde08b400450/scratchpad/"
|
||||
SECTIONS = {k: f"Section / Admin / {k.title()}" for k in ["auth", "dashboard", "configuration", "criteria", "schedule"]}
|
||||
LIB_Y = 1400 # my new main components live here (above my sections)
|
||||
|
||||
|
||||
def fg(c):
|
||||
return f"var(--color-{c})"
|
||||
|
||||
|
||||
def I(comp, name=None, style="", **ov):
|
||||
"""Instance html with overrides and optional root style."""
|
||||
o = json.dumps(ov, ensure_ascii=False).replace("'", "'")
|
||||
n = f' layer-name="{name}"' if name else ""
|
||||
s = f' style="{style}"' if style else ""
|
||||
return f"<x-paper-instance component=\"{comp}\"{n}{s} overrides='{o}'/>"
|
||||
|
||||
|
||||
def card(name, kids, w=None, pad="var(--spacing-lg)", gap="16px", **st):
|
||||
if w:
|
||||
st["width"] = f"{w}px"
|
||||
return col(name, kids, padding=pad, gap=gap, background=fg("bg-elevated"), border="1px solid var(--color-border-light)",
|
||||
border_radius="var(--radius-lg)", box_shadow="var(--shadow-sm)", **st)
|
||||
|
||||
|
||||
def div(name="Divider"):
|
||||
return row(name, "", height="1px", background=fg("border-light"), align_self="stretch")
|
||||
|
||||
|
||||
def h5(t, name="Card Title"):
|
||||
return T(name, t, 16, fg("text-primary"), 600, lh=24)
|
||||
|
||||
|
||||
def sec(t, name="Description", mb=None):
|
||||
return T(name, t, 14, fg("text-secondary"))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ shell
|
||||
def screen(name, route, active, title, sub, content, wide=False, actions="", overlay="", centered=True):
|
||||
"""Screen / <name> 1440x900: sidebar instance + header instance + content area."""
|
||||
header = I("Admin Shell Header", "Admin Shell Header", "width:1192px", Title=title, Subtitle=sub)
|
||||
if actions:
|
||||
header = col("Header Wrap", header + row("Header Actions", actions, position="absolute", right="72px", top="34px"),
|
||||
position="relative", width="1192px", height="102px")
|
||||
inner_w = "1100px" if wide else "900px"
|
||||
inner = col("Content Inner", content, width=inner_w, gap="0px")
|
||||
body = col("Content", inner, align_items="center", padding="var(--page-padding)", flex_grow="1", overflow="hidden", background=fg("bg-layout"), width="1192px")
|
||||
main = col("Main", header + body, width="1192px", height="900px", background=fg("bg-layout"), overflow="hidden")
|
||||
kids = I(f"Admin Sidebar / {active} Active", "Admin Sidebar", "") + main
|
||||
if overlay:
|
||||
kids += row("Overlay", overlay, position="absolute", left="0px", top="0px", width="1440px", height="900px",
|
||||
background=fg("bg-overlay"), justify_content="center", align_items="center")
|
||||
frame = row(f"Screen / {name}", kids, width="1440px", height="900px", background=fg("bg-layout"), overflow="hidden", position="relative", align_items="stretch")
|
||||
return col(f"Screen Wrap / {name}", T("Route Caption", route, 12, fg("text-secondary"), lh=16) + frame, gap="8px")
|
||||
|
||||
|
||||
def raw_screen(name, route, content, bg="var(--color-bg-layout)"):
|
||||
frame = col(f"Screen / {name}", content, width="1440px", height="900px", background=bg, overflow="hidden", position="relative")
|
||||
return col(f"Screen Wrap / {name}", T("Route Caption", route, 12, fg("text-secondary"), lh=16) + frame, gap="8px")
|
||||
|
||||
|
||||
def section(name, title, screens):
|
||||
return row(name, col("Section Body", T("Section Title", title, 24, fg("text-primary"), 600, lh=32) + row("Screens", "".join(screens), gap="var(--spacing-xl)", align_items="flex-start"),
|
||||
gap="var(--spacing-xl)"), padding="var(--spacing-xl)", background="transparent", width="fit-content")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ new main components (to promote)
|
||||
def donut_svg(segs, size=132, th=16):
|
||||
r = (size - th) / 2
|
||||
c = size / 2
|
||||
tot = sum(v for v, _ in segs)
|
||||
a0 = -math.pi / 2
|
||||
paths = ""
|
||||
for v, col_ in segs:
|
||||
a1 = a0 + v / tot * 2 * math.pi
|
||||
large = 1 if (a1 - a0) > math.pi else 0
|
||||
paths += f'<path d="M{c + r * math.cos(a0):.2f} {c + r * math.sin(a0):.2f} A{r} {r} 0 {large} 1 {c + r * math.cos(a1):.2f} {c + r * math.sin(a1):.2f}" fill="none" stroke="{col_}" stroke-width="{th}"/>'
|
||||
a0 = a1
|
||||
ring = f'<svg layer-name="Ring" width="{size}" height="{size}" viewBox="0 0 {size} {size}"><circle cx="{c}" cy="{c}" r="{r}" fill="none" stroke="#f0f0f0" stroke-width="{th}"/>{paths}</svg>'
|
||||
center = col("Center", T("Total", "11", 26, fg("text-primary"), 700, lh=32) + T("Caption", "sessions", 12, fg("text-secondary"), lh=16), align_items="center",
|
||||
justify_content="center", position="absolute", left="0px", top="0px", width=f"{size}px", height=f"{size}px")
|
||||
return ring + center
|
||||
|
||||
|
||||
def avatar(initials, size, color, name="Avatar"):
|
||||
fs = {30: 12, 36: 14}.get(size, 12)
|
||||
return I(f"Avatar / {size}", name, f"background-color:{color}", Initials=initials)
|
||||
|
||||
|
||||
def comp_specs():
|
||||
"""[(name, html, width)] of new main components."""
|
||||
L = []
|
||||
L.append(("Funnel Row", row("Funnel Row", T("Label", "Applied", 12, fg("text-secondary"), lh=16, width="90px", text_align="right") +
|
||||
row("Track", row("Fill", T("Count", "28", 12, fg("text-primary"), 600, lh=16), width="362px", height="26px", padding="0 10px", background="#bae0ff", border_radius="var(--radius-sm)"),
|
||||
flex_grow="1", height="26px", background=fg("bg-layout"), border_radius="var(--radius-sm)", overflow="hidden"), gap="12px", width="464px")))
|
||||
L.append(("Funnel Row / Inverse", row("Funnel Row / Inverse", T("Label", "Onsite", 12, fg("text-secondary"), lh=16, width="90px", text_align="right") +
|
||||
row("Track", row("Fill", T("Count", "9", 12, fg("text-inverse"), 600, lh=16), width="116px", height="26px", padding="0 10px", background="#4096ff", border_radius="var(--radius-sm)"),
|
||||
flex_grow="1", height="26px", background=fg("bg-layout"), border_radius="var(--radius-sm)", overflow="hidden"), gap="12px", width="464px")))
|
||||
L.append(("Challenge Usage Row", col("Challenge Usage Row", row("Head", T("Title", "Two-sum with streaming input", 14, fg("text-primary"), 500, flex_grow="1") +
|
||||
T("Count", "18", 12, fg("text-secondary"), 600, lh=16), justify_content="space-between", gap="8px") +
|
||||
row("Track", row("Fill", "", width="196px", height="5px", background=fg("brand-primary"), border_radius="99px"), width="196px", height="5px", background=fg("border-light"), border_radius="99px", overflow="hidden"),
|
||||
gap="6px", width="196px")))
|
||||
L.append(("Review Item", row("Review Item", I("Avatar / 30", "Avatar", "", Initials="PI") +
|
||||
col("Texts", T("Name", "Priya Iyer", 14, fg("text-primary"), 600) + T("Meta", "Live coding · 45m", 12, fg("text-secondary"), lh=16), flex_grow="1") +
|
||||
I("Button / Primary Small", "Score Button", "", Label="Score"), gap="12px", padding="8px 12px", width="316px", background=fg("bg-elevated"),
|
||||
border="1px solid var(--color-border-light)", border_radius="var(--radius-md)")))
|
||||
for st, tone in [("Scheduled", "Blue"), ("In Progress", "Gold")]:
|
||||
L.append((f"Upcoming Row / {st}", row(f"Upcoming Row / {st}", box("Accent Bar", "", width="4px", height="36px", border_radius="var(--radius-sm)", background=fg("brand-primary")) +
|
||||
I("Avatar / 36", "Avatar", "", Initials="SA") +
|
||||
col("Texts", T("Name", "Sara Ahmed", 14, fg("text-primary"), 600) + T("Meta", "Live coding · 45m", 12, fg("text-secondary"), lh=16), flex_grow="1") +
|
||||
col("When Col", T("When", "Today · 14:00", 12, fg("text-secondary"), lh=16, text_align="right") + I(f"Tag Dot / {tone}", "Status Tag", "", Label=st), align_items="flex-end", gap="4px"),
|
||||
gap="12px", padding="12px 0px", width="316px")))
|
||||
L.append(("Donut / Score Distribution", box("Donut / Score Distribution", donut_svg([(3, "#52c41a"), (6, "#1677ff"), (2, "#faad14")]), position="relative", display="flex", width="132px", height="132px")))
|
||||
L.append(("Legend Row", row("Legend Row", box("Swatch", "", width="10px", height="10px", border_radius="3px", background=fg("success")) +
|
||||
T("Label", "Strong (4.5+)", 14, fg("text-secondary"), flex_grow="1") + T("Value", "3", 14, fg("text-primary"), 700), gap="8px", width="196px")))
|
||||
L.append(("Criterion Row / Weight", row("Criterion Row / Weight", T("Label", "Problem solving", 14, fg("text-primary"), 500, flex_grow="1") +
|
||||
row("Weight Input", T("Value", "30", 14, fg("text-primary"), flex_grow="1") + T("Suffix", "%", 14, fg("text-secondary")), width="80px", height="24px", padding="0 7px", gap="4px",
|
||||
background=fg("bg-elevated"), border="1px solid var(--color-border)", border_radius="var(--radius-md)") +
|
||||
I("Ant Button / Text Danger Small", "Remove Button", "", Label="×"), gap="16px", padding="8px 16px", width="512px", background=fg("bg-layout"), border_radius="var(--radius-md)")))
|
||||
for on in (True, False):
|
||||
nm = f"Setting Row / {'On' if on else 'Off'}"
|
||||
L.append((nm, col(nm, row("Row", col("Texts", T("Label", "Session created", 14, fg("text-primary"), 500) + T("Desc", "When a new interview session is scheduled", 12, fg("text-secondary"), lh=16), gap="2px", flex_grow="1") +
|
||||
I("Switch / On" if on else "Switch / Off", "Switch", ""), gap="16px", padding="16px 0px", width="512px"))))
|
||||
for av in (True, False):
|
||||
nm = f"ATS Tile / {'Available' if av else 'Soon'}"
|
||||
L.append((nm, row(nm, T("Name", "Greenhouse", 14, fg("text-primary"), 600, flex_grow="1") +
|
||||
(I("Ant Button / Default Small", "Connect Button", "", Label="Connect") if av else I("Ant Tag / Default", "Soon Tag", "", Label="Soon")),
|
||||
justify_content="space-between", padding="16px", width="248px", border="1px solid var(--color-border-light)", border_radius="var(--radius-md)")))
|
||||
L.append(("Pipeline Row / Dark", row("Pipeline Row / Dark", row("Who", box("Dot", "", width="8px", height="8px", border_radius="99px", background="#4ade80") + T("Name", "Amara Okafor", 14, fg("sidebar-text")), gap="8px") +
|
||||
T("Stage", "Onsite loop", 12, fg("sidebar-text-faint"), lh=16), justify_content="space-between", padding="8px 0px", width="100%")))
|
||||
return L
|
||||
|
||||
|
||||
def build_components(P):
|
||||
names = [n for n, _ in comp_specs()]
|
||||
root = P.c("get_children", nodeId="root")["children"]
|
||||
ids = [c["id"] for c in root if c["name"] in names]
|
||||
if ids:
|
||||
P.c("delete_nodes", nodeIds=ids)
|
||||
x = 0
|
||||
for n, html in comp_specs():
|
||||
r = P.c("write_html", targetNodeId="root", mode="insert-children", html=html)
|
||||
for k in ("warnings", "ignoredStyles"):
|
||||
if r.get(k):
|
||||
print(" ", n, k, r[k][:3] if isinstance(r[k], list) else r[k])
|
||||
nid = r["createdNodeIds"][0]
|
||||
P.c("create_component", nodeId=nid, name=n)
|
||||
P.c("update_styles", updates=[{"nodeIds": [nid], "styles": {"left": x, "top": LIB_Y}}])
|
||||
x += 360
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ placement
|
||||
def place(P, name, html, top):
|
||||
root = P.c("get_children", nodeId="root")["children"]
|
||||
old = [c["id"] for c in root if c["name"] == name]
|
||||
if old:
|
||||
P.c("delete_nodes", nodeIds=old)
|
||||
r = P.add("root", html)
|
||||
P.c("update_styles", updates=[{"nodeIds": [r[0]], "styles": {"left": 0, "top": top}}])
|
||||
info = P.c("get_node_info", nodeId=r[0])
|
||||
return r[0], top + info.get("height", 1200) + 200
|
||||
|
||||
|
||||
def start_top(P):
|
||||
root = P.c("get_children", nodeId="root")["children"]
|
||||
mine = set(SECTIONS.values())
|
||||
tops = [c.get("y", 0) + c["height"] for c in root if c["name"].startswith("Section / ") and c["name"] not in mine and c.get("x", 0) < 20000]
|
||||
return max(tops + [2400]) + 200
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ AUTH
|
||||
def brand_mark():
|
||||
return row("Brand", box("Brand Mark", T("Brand Mark Letter", "H", 14, fg("text-inverse"), 700, lh=18), display="flex", justify_content="center", align_items="center",
|
||||
width="28px", height="28px", border_radius="var(--radius-md)", background=fg("brand-primary")) +
|
||||
T("Brand Name", "Hireloop", 16, fg("text-inverse"), 700, lh=20), gap="10px")
|
||||
|
||||
|
||||
def pipeline_card(w=None):
|
||||
rows = "".join(I("Pipeline Row / Dark", f"Pipeline Row / {n}", "width:100%", Name=n, Stage=s) for n, s in
|
||||
[("Amara Okafor", "Onsite loop"), ("Mei Lin Tan", "Debrief"), ("Sofia Bianchi", "Offer")])
|
||||
head = row("Pipeline Header", T("Pipeline Title", "CANDIDATE PIPELINE", 12, fg("sidebar-text-faint"), 700, lh=16, letter_spacing="0.84px") +
|
||||
T("Live Pill", "Live", 12, "#4ade80", 600, lh=16, padding="2px 8px", background="#16a34a2e", border_radius="99px"), justify_content="space-between")
|
||||
return col("Pipeline Card", head + rows, gap="8px", padding="16px", background="#ffffff0d", border="1px solid #ffffff14", border_radius="10px", **({"width": f"{w}px"} if w else {}))
|
||||
|
||||
|
||||
FEATURES = [("squares-four", "Team-wide visibility", "Track every candidate across the loop, from screen to offer."),
|
||||
("brackets-curly", "Live coding & system design", "Watch candidates work in a real shared editor and whiteboard."),
|
||||
("diamond", "Calibrated scorecards", "A shared rubric keeps every interviewer's ratings comparable.")]
|
||||
|
||||
|
||||
def login_screen(error=False):
|
||||
feats = "".join(row(f"Feature / {t}", box("Feature Icon", ic(i, 16, fg("sidebar-text")), display="flex", justify_content="center", align_items="center", width="32px", height="32px",
|
||||
border_radius="6px", background="#ffffff12") +
|
||||
col("Texts", T("Feature Title", t, 14, fg("text-inverse"), 600, lh=18) + T("Feature Desc", d, 12, fg("sidebar-text-muted"), lh=18), gap="2px", flex_grow="1"),
|
||||
gap="16px", align_items="flex-start") for i, t, d in FEATURES)
|
||||
hero = col("Hero", T("Hero Title", "Run better interviews, together.", 32, fg("text-inverse"), 800, lh=38, letter_spacing="-0.6px") +
|
||||
T("Hero Sub", "Structured scorecards, live coding sessions, and calibrated hiring decisions — all in one workspace for your team.", 14, fg("sidebar-text-muted"), lh=24), gap="16px")
|
||||
mid = col("Marketing Body", hero + col("Features", feats, gap="20px") + pipeline_card(), gap="32px", flex_grow="1", justify_content="center", padding="56px 0px 0px")
|
||||
left = col("Marketing Panel", brand_mark() + mid + T("Footer Text", "© 2026 Hireloop. All rights reserved.", 12, "#ffffff38", lh=16), padding="32px", width="662px", height="900px", background="#0f1117")
|
||||
alert = row("Alert / Error", ic("x-circle", 16, fg("score-poor"), "Alert Icon") + T("Message", "Invalid credentials", 14, fg("text-primary"), flex_grow="1"), gap="8px", width="400px",
|
||||
padding="8px 12px", background=fg("tag-red-bg"), border="1px solid var(--color-tag-red-bd)", border_radius="var(--radius-md)") if error else ""
|
||||
pw_label = row("Password Label", T("Label", "Password", 14, fg("text-primary"), lh=22) + T("Forgot", "Forgot password?", 13, fg("text-link"), lh=18), justify_content="space-between", width="400px")
|
||||
form = col("Sign In Form", col("Heading", T("Title", "Sign in to your workspace", 30, fg("text-primary"), 700, lh=38, letter_spacing="-0.3px") +
|
||||
T("Subtitle", "Enter your credentials to continue to Hireloop.", 14, fg("text-secondary")), gap="4px") + alert +
|
||||
col("Fields", I("Form Field / Required", "Form Field / Work email", "width:400px", Label="Work email", Value="you@company.com") +
|
||||
col("Form Field / Password", pw_label + I("Input / Password", "Input", "width:400px"), gap="8px"), gap="24px") +
|
||||
I("Checkbox / Unchecked", "Remember", "", Label="Keep me signed in") +
|
||||
I("Ant Button / Primary Large", "Sign In Button", "width:400px", Label="Sign in") +
|
||||
row("Links", T("Request", "Don't have access? ", 13, fg("text-secondary"), lh=18) + T("Invite Link", "Ask your workspace admin for an invite", 13, fg("text-link"), lh=18), justify_content="center", width="400px"),
|
||||
gap="24px", width="400px")
|
||||
right = col("Form Panel", form, flex_grow="1", justify_content="center", align_items="center", background=fg("bg-elevated"), padding="48px", height="900px")
|
||||
return row("Login Split", left + right, width="1440px", height="900px")
|
||||
|
||||
|
||||
def landing_screen():
|
||||
nav = row("Nav", brand_mark() + I("Ant Button / Primary", "Sign In Button", "", Label="Sign in"), justify_content="space-between")
|
||||
eyebrow = row("Eyebrow", box("Dot", "", width="6px", height="6px", border_radius="99px", background="#4ade80") +
|
||||
T("Eyebrow Label", "INTERVIEW PLATFORM FOR HIRING TEAMS", 12, fg("sidebar-text"), 700, lh=16, letter_spacing="1.2px"),
|
||||
gap="8px", padding="6px 12px", background="#ffffff0a", border="1px solid #ffffff14", border_radius="99px", align_self="flex-start")
|
||||
title = row("Hero Title", T("Title Plain", "Run better interviews, ", 44, fg("text-inverse"), 800, lh=48, letter_spacing="-1.3px") +
|
||||
T("Title Accent", "together.", 44, fg("brand-secondary"), 800, lh=48, letter_spacing="-1.3px"), gap="0px", flex_wrap="wrap")
|
||||
stats = row("Stats", "".join(col(f"Stat / {l}", T("Value", v, 28, fg("text-inverse"), 800, lh=32) + T("Label", l, 12, fg("sidebar-text-muted"), lh=16), gap="2px")
|
||||
for v, l in [("4.1", "Avg. rubric score"), ("34%", "Offer rate"), ("12", "Active candidates")]), gap="36px")
|
||||
hero = col("Hero", eyebrow + col("Title Block", title, width="580px") +
|
||||
T("Hero Sub", "Structured scorecards, live coding sessions, and calibrated hiring decisions — all in one workspace for your team.", 16, fg("sidebar-text-muted"), lh=26, width="540px") +
|
||||
row("CTA Row", I("Ant Button / Primary Large", "Sign In Button", "", Label="Sign in to your workspace") +
|
||||
T("Hint", "No access yet? Ask your workspace admin.", 13, fg("sidebar-text-muted"), lh=18), gap="16px") + stats,
|
||||
gap="20px", width="620px")
|
||||
hero_row = row("Hero Row", hero + pipeline_card(320), gap="48px", flex_grow="1", align_items="center")
|
||||
grid = row("Feature Grid", "".join(col(f"Feature / {t}", box("Feature Icon", ic(i, 20, fg("brand-secondary")), display="flex", justify_content="center", align_items="center",
|
||||
width="36px", height="36px", border_radius="var(--radius-md)", background="#69b1ff1f") +
|
||||
T("Feature Title", t, 14, fg("text-inverse"), 600, lh=20) + T("Feature Desc", d, 14, fg("sidebar-text-muted"), lh=22), gap="8px", padding="20px", flex_grow="1",
|
||||
background="#ffffff0a", border="1px solid #ffffff12", border_radius="var(--radius-lg)") for i, t, d in FEATURES), gap="16px")
|
||||
foot = row("Footer", T("Left", "© 2026 Hireloop. All rights reserved.", 12, "#ffffff40", lh=16) + T("Right", "Built for structured, calibrated hiring.", 12, "#ffffff40", lh=16),
|
||||
justify_content="space-between", padding="16px 0px 0px", border="1px solid transparent")
|
||||
glow = box("Glow", "", position="absolute", left="340px", top="-160px", width="760px", height="560px", background="radial-gradient(circle, #1677ff47 0%, #1677ff14 40%, transparent 70%)")
|
||||
inner = col("Landing Inner", nav + hero_row + grid + foot, width="1080px", padding="28px 40px", gap="24px", height="900px", position="absolute", left="180px", top="0px")
|
||||
return box("Landing", glow + inner, position="relative", width="1440px", height="900px", background="#0f1117", overflow="hidden")
|
||||
|
||||
|
||||
def build_auth():
|
||||
return section(SECTIONS["auth"], "Admin / Auth", [
|
||||
raw_screen("Admin Login", "/login", login_screen()),
|
||||
raw_screen("Admin Login Error", "/login (invalid credentials)", login_screen(True)),
|
||||
raw_screen("Admin Landing", "/", landing_screen(), bg="#0f1117"),
|
||||
])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ DASHBOARD
|
||||
def stats_row(vals):
|
||||
comps = ["Blue", "Purple", "Orange Down", "Green", "Teal"]
|
||||
labels = ["ACTIVE CANDIDATES", "INTERVIEWS SCHEDULED", "AWAITING REVIEW", "AVG. SCORE", "OFFER RATE"]
|
||||
out = ""
|
||||
for c, l, (v, d, t) in zip(comps, labels, vals):
|
||||
arrow = "▼" if c == "Orange Down" else "▲"
|
||||
out += I(f"Stat / {c}", f"Stat / {l.title()}", "width:167px", Label=l, Value=v, Delta=d, Trend=t, **{"Delta Arrow": arrow})
|
||||
return row("Stats Row", out, gap="var(--spacing-md)", align_items="stretch", width="900px")
|
||||
|
||||
|
||||
FUNNEL = [("Applied", 28, "#bae0ff"), ("Screen", 18, "#91caff"), ("Phone", 14, "#69b1ff"), ("Onsite", 9, "var(--color-button-primary-hover)"), ("Offer", 5, "var(--color-brand-primary)"), ("Hired", 3, "#0958d9")]
|
||||
TOP = [("Two-sum with streaming input", 18, "var(--color-brand-primary)"), ("REST API: pagination + caching", 14, "var(--color-avatar-purple)"),
|
||||
("SQL: window functions deep-dive", 11, "var(--color-avatar-teal)"), ("Build a Markdown previewer", 8, "var(--color-warning)"), ("Debug a memory leak", 5, "var(--color-success)")]
|
||||
|
||||
|
||||
def funnel_card(empty=False):
|
||||
rows = ""
|
||||
for i, (s, n, c) in enumerate(FUNNEL):
|
||||
nn = 0 if empty else n
|
||||
w = max(0.06, nn / 28) * 362
|
||||
rows += I("Funnel Row" if i < 3 else "Funnel Row / Inverse", f"Funnel Row / {s}", "", Label=s, Count=str(nn), Fill={"width": f"{w:.0f}px", "backgroundColor": c})
|
||||
return card("Card / Hiring pipeline", I("Section Title", "Section Title", "width:464px", Title="Hiring pipeline", Subtitle="Across all open roles") +
|
||||
col("Funnel", rows, gap="10px"), w=512, gap="16px")
|
||||
|
||||
|
||||
def score_card(empty=False):
|
||||
if empty:
|
||||
body = I("Empty / Simple", "Empty", "width:196px", Description="No completed sessions")
|
||||
else:
|
||||
body = col("Donut Block", row("Donut Wrap", I("Donut / Score Distribution", "Donut", ""), justify_content="center", width="196px") +
|
||||
col("Legend", I("Legend Row", "Legend / Strong", "", Label="Strong (4.5+)", Value="3") +
|
||||
I("Legend Row", "Legend / Solid", "", Label="Solid (3.5–4.5)", Value="6", Swatch={"backgroundColor": fg("brand-primary")}) +
|
||||
I("Legend Row", "Legend / Borderline", "", Label="Borderline (<3.5)", Value="2", Swatch={"backgroundColor": fg("score-warn")}), gap="12px"), gap="16px")
|
||||
return card("Card / Score distribution", I("Section Title", "Section Title", "width:196px", Title="Score distribution", Subtitle="Completed sessions") + body, w=244)
|
||||
|
||||
|
||||
def top_card(empty=False):
|
||||
if empty:
|
||||
body = I("Empty / Simple", "Empty", "width:196px", Description="No challenge usage yet")
|
||||
else:
|
||||
body = col("Usage List", "".join(I("Challenge Usage Row", f"Usage / {t}", "", Title=t, Count=str(n), Fill={"width": f"{n / 18 * 196:.0f}px", "backgroundColor": c}) for t, n, c in TOP), gap="12px")
|
||||
return card("Card / Most-used challenges", I("Section Title / With Action", "Section Title", "width:196px", Title="Most-used challenges", Action="Library →", Subtitle={"display": "none"}) + body, w=244)
|
||||
|
||||
|
||||
def review_card(empty=False):
|
||||
head = row("Review Head", col("Texts", T("Title", "Awaiting review", 16, fg("text-primary"), 600, lh=22) +
|
||||
T("Subtitle", "0 sessions need your scorecard" if empty else "2 sessions need your scorecard", 12, fg("text-secondary"), lh=16)) +
|
||||
("" if empty else I("Tag Dot / Gold", "Action Tag", "", Label="Action needed")), justify_content="space-between", gap="8px")
|
||||
if empty:
|
||||
items = I("Empty State", "Empty State", "width:316px", Title="All caught up", Description="No sessions are waiting for your scorecard.")
|
||||
else:
|
||||
items = col("Review Items", I("Review Item", "Review Item / Priya Iyer", "", Name="Priya Iyer", Meta="Live coding · 45m", Avatar={"backgroundColor": fg("avatar-orange")}, Initials="PI") +
|
||||
I("Review Item", "Review Item / Marcus Chen", "", Name="Marcus Chen", Meta="Fullstack · 60m", Avatar={"backgroundColor": fg("warning")}, Initials="MC"), gap="8px")
|
||||
return card("Card / Awaiting review", head + items, w=364, background="linear-gradient(180deg, #fffdf5, #ffffff)")
|
||||
|
||||
|
||||
UPC = [("Sara Ahmed", "SA", "Live coding", 45, "Today · 14:00", "Scheduled", "brand-primary", "avatar-teal"),
|
||||
("Tomás López", "TL", "Fullstack", 60, "Today · 16:30", "Scheduled", "avatar-purple", "avatar-teal"),
|
||||
("Aisha Khan", "AK", "SQL", 30, "Tomorrow · 10:00", "Scheduled", "avatar-teal", "avatar-pink"),
|
||||
("Owen Park", "OP", "Live coding", 45, "Thu · 11:00", "In Progress", "brand-primary", "avatar-indigo")]
|
||||
|
||||
|
||||
def upcoming_card(empty=False):
|
||||
head = I("Section Title", "Section Title", "width:316px", Title="Upcoming interviews", Subtitle={"display": "none"})
|
||||
if empty:
|
||||
return card("Card / Upcoming interviews", head + I("Empty / Simple", "Empty", "width:316px", Description="No interviews scheduled"), w=364)
|
||||
rows = ""
|
||||
for i, (n, ini, t, m, when, st, acc, avc) in enumerate(UPC):
|
||||
if i:
|
||||
rows += div()
|
||||
rows += I(f"Upcoming Row / {st}", f"Upcoming Row / {n}", "", Name=n, Initials=ini, Meta=f"{t} · {m}m", When=when, **{"Accent Bar": {"backgroundColor": fg(acc)}, "Avatar": {"backgroundColor": fg(avc)}})
|
||||
return card("Card / Upcoming interviews", head + col("Upcoming List", rows, gap="0px"), w=364, gap="8px")
|
||||
|
||||
|
||||
def dashboard_content(empty=False):
|
||||
st = stats_row([("0", "0", "this week")] * 5 if empty else [("12", "+3", "this week"), ("8", "+2", "vs last wk"), ("2", "−1", "cleared"), ("4.1", "+0.2", "this month"), ("34%", "+5%", "this quarter")])
|
||||
left = col("Left Column", funnel_card(empty) + row("Pair", score_card(empty) + top_card(empty), gap="var(--spacing-lg)", align_items="flex-start"), gap="var(--spacing-lg)")
|
||||
right = col("Right Column", review_card(empty) + upcoming_card(empty), gap="var(--spacing-lg)")
|
||||
return col("Dashboard", st + row("Dashboard Grid", left + right, gap="var(--spacing-lg)", align_items="flex-start"), gap="var(--spacing-lg)")
|
||||
|
||||
|
||||
def build_dashboard():
|
||||
return section(SECTIONS["dashboard"], "Admin / Dashboard", [
|
||||
screen("Admin Dashboard", "/dashboard", "Dashboard", "Good afternoon", "Welcome to your dashboard", dashboard_content()),
|
||||
screen("Admin Dashboard Empty", "/dashboard (no data)", "Dashboard", "Good afternoon", "Welcome to your dashboard", dashboard_content(True)),
|
||||
])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ CONFIGURATION
|
||||
def field(label, control, w=512, required=False):
|
||||
lab = (T("Required Mark", "*", 14, fg("error"), lh=22) if required else "") + T("Label", label, 14, fg("text-primary"), lh=22)
|
||||
return col(f"Field / {label}", row("Label Row", lab, gap="4px") + control, gap="8px", width=f"{w}px")
|
||||
|
||||
|
||||
def cfg_screen(tab_idx, name, route, content):
|
||||
tabs = I(f"Tabs / Bar 5 Large Active {tab_idx}", "Tabs", "width:1100px", **{"Tab 1 Label": "Workspace", "Tab 2 Label": "Team", "Tab 3 Label": "Scoring", "Tab 4 Label": "Notifications", "Tab 5 Label": "Integrations"})
|
||||
return screen(name, route, "Configuration", "Configuration", "Workspace settings, team, scoring & integrations", col("Config", tabs + col("Tab Body", content, padding="16px 0px 0px"), gap="0px"), wide=True)
|
||||
|
||||
|
||||
def tab_workspace():
|
||||
general = card("Card / General", h5("General") + col("Form", field("Workspace name", I("Input / Large Filled", "Input", "width:512px", Value="Netcracker"), required=True) +
|
||||
field("Workspace URL", I("Input / With Prefix", "Input", "width:512px", Prefix="hireloop.io/", Value={"color": fg("text-primary")})) +
|
||||
field("Default timezone", I("Select / Large Filled", "Select", "width:512px", Value="UTC+3 (Moscow)")) +
|
||||
field("Description", I("Textarea", "Textarea", "width:512px", Value="Brief description of your team or hiring process")), gap="24px") +
|
||||
I("Ant Button / Primary", "Save Button", "", Label="Save changes"), w=560)
|
||||
danger = card("Card / Danger zone", col("Texts", h5("Danger zone") + T("Description", "Irreversible actions. Proceed with caution.", 14, fg("text-secondary")), gap="4px") +
|
||||
I("Ant Button / Danger", "Delete Button", "", Label="Delete workspace"), w=560)
|
||||
return col("Workspace Tab", general + danger, gap="20px")
|
||||
|
||||
|
||||
MEMBERS = [("Elena Vasquez", "elena.vasquez@netcracker.com", "Admin", "Active"), ("Dmitri Sokolov", "d.sokolov@netcracker.com", "Interviewer", "Active"),
|
||||
("Priya Nair", "p.nair@netcracker.com", "Interviewer", "Active"), ("James Obi", "j.obi@netcracker.com", "Viewer", "Pending")]
|
||||
ROLE_TAG = {"Admin": "Tag / Blue", "Interviewer": "Tag / Purple", "Viewer": "Ant Tag / Default"}
|
||||
|
||||
|
||||
def th_row(cols, widths, name="Header Row"):
|
||||
cells = "".join(T(f"Col {i}", c, 14, fg("text-primary"), 600, **({"width": f"{w}px"} if w else {"flex_grow": "1"})) for i, (c, w) in enumerate(zip(cols, widths), 1))
|
||||
return row(name, cells, gap="16px", padding="16px", background=fg("bg-sider"), align_self="stretch")
|
||||
|
||||
|
||||
def td_row(name, cells, widths, pad="16px"):
|
||||
out = "".join(row(f"Cell {i}", c, **({"width": f"{w}px"} if w else {"flex_grow": "1"})) for i, (c, w) in enumerate(zip(cells, widths), 1))
|
||||
return row(name, out, gap="16px", padding=pad, align_self="stretch", background=fg("bg-elevated"))
|
||||
|
||||
|
||||
def tab_team():
|
||||
W = [None, 120, 120, 160]
|
||||
rows = th_row(["Name", "Role", "Status", ""], W)
|
||||
for n, e, r, s in MEMBERS:
|
||||
rows += div() + td_row(f"Row / {n}", [col("Name Cell", T("Name", n, 14, fg("text-primary"), 600) + T("Email", e, 12, fg("text-secondary"), lh=16), gap="0px"),
|
||||
I(ROLE_TAG[r], "Role Tag", "", Label=r), I("Ant Tag / Success" if s == "Active" else "Ant Tag / Default", "Status Tag", "", Label=s),
|
||||
row("Actions", I("Ant Button / Text Small", "Edit", "", Label="Edit") + I("Ant Button / Text Danger Small", "Remove", "", Label="Remove"), gap="4px", justify_content="flex-end")], W)
|
||||
table = col("Table", rows, width="700px", background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)", overflow="hidden", box_shadow="var(--shadow-sm)")
|
||||
top = row("Team Head", T("Count", "4 members in this workspace", 14, fg("text-secondary")) + I("Ant Button / Primary", "Invite Button", "", Label="Invite member"), justify_content="space-between", width="700px")
|
||||
return col("Team Tab", top + table, gap="20px")
|
||||
|
||||
|
||||
CRIT = [("Problem solving", 30), ("Code quality", 25), ("Communication", 20), ("System design", 15), ("Culture fit", 10)]
|
||||
|
||||
|
||||
def tab_scoring():
|
||||
scale = card("Card / Scoring scale", col("Texts", h5("Scoring scale") + T("Description", "All scorecard ratings use this scale.", 14, fg("text-secondary")), gap="4px") +
|
||||
row("Scale Form", row("Min Field", T("Label", "Min:", 14, fg("text-primary")) + I("Input / Large Filled", "Input", "width:72px", Value="1"), gap="8px") +
|
||||
row("Max Field", T("Label", "Max:", 14, fg("text-primary")) + I("Input / Large Filled", "Input", "width:72px", Value="5"), gap="8px") +
|
||||
row("Step Field", T("Label", "Step:", 14, fg("text-primary")) + I("Select / Large Filled", "Select", "width:88px", Value="1"), gap="8px"), gap="16px"), w=600)
|
||||
items = "".join(I("Criterion Row / Weight", f"Criterion Row / {l}", "", Label=l, Value=str(w)) for l, w in CRIT)
|
||||
rub = card("Card / Rubric criteria", row("Rubric Head", col("Texts", h5("Rubric criteria") + T("Description", "Weights must sum to 100%.", 14, fg("text-secondary")), gap="4px") +
|
||||
I("Ant Button / Default", "Add Button", "", Label="Add criterion"), justify_content="space-between", align_items="flex-start") +
|
||||
col("Criteria", items, gap="8px") + I("Ant Button / Primary", "Save Button", "", Label="Save rubric"), w=600)
|
||||
return col("Scoring Tab", scale + rub, gap="20px")
|
||||
|
||||
|
||||
NOTIFS = [("Session created", "When a new interview session is scheduled", True), ("Candidate joined", "When a candidate enters their session", True),
|
||||
("Review ready", "When all scorecards are submitted for a session", True), ("Invite expired", "When a candidate invite link expires unused", False),
|
||||
("Weekly digest", "Summary of sessions, scores, and pipeline activity", True)]
|
||||
|
||||
|
||||
def tab_notifications():
|
||||
rows = ""
|
||||
for i, (l, d, on) in enumerate(NOTIFS):
|
||||
rows += I(f"Setting Row / {'On' if on else 'Off'}", f"Setting Row / {l}", "", Label=l, Desc=d)
|
||||
if i < len(NOTIFS) - 1:
|
||||
rows += div()
|
||||
c = card("Card / Email notifications", col("Texts", h5("Email notifications") + T("Description", "Sent to your work email. You can override per-session in session settings.", 14, fg("text-secondary")), gap="4px") +
|
||||
col("Settings", rows, gap="0px") + I("Ant Button / Primary", "Save Button", "", Label="Save preferences"), w=560)
|
||||
return c
|
||||
|
||||
|
||||
def tab_integrations():
|
||||
hook = card("Card / Webhook", col("Texts", h5("Webhook") + T("Description", "POST events (session created, review complete) to your endpoint.", 14, fg("text-secondary")), gap="4px") +
|
||||
col("Form", field("Endpoint URL", I("Input / Large", "Input", "width:552px", Value="https://your-app.com/webhooks/hireloop"), 552) +
|
||||
field("Signing secret", I("Input / Password", "Input", "width:552px"), 552), gap="24px") + I("Ant Button / Primary", "Save Button", "", Label="Save webhook"), w=600)
|
||||
tiles = [("Greenhouse", True), ("Lever", True), ("Workday", False), ("BambooHR", False)]
|
||||
t = "".join(I(f"ATS Tile / {'Available' if a else 'Soon'}", f"ATS Tile / {n}", "", Name=n) for n, a in tiles)
|
||||
ats = card("Card / ATS integrations", col("Texts", h5("ATS integrations") + T("Description", "Sync candidates and decisions directly with your applicant tracking system.", 14, fg("text-secondary")), gap="4px") +
|
||||
row("ATS Grid", t, flex_wrap="wrap", gap="12px", width="552px"), w=600)
|
||||
return col("Integrations Tab", hook + ats, gap="20px")
|
||||
|
||||
|
||||
def build_configuration():
|
||||
return section(SECTIONS["configuration"], "Admin / Configuration", [
|
||||
cfg_screen(1, "Admin Configuration Workspace", "/configuration (Workspace)", tab_workspace()),
|
||||
cfg_screen(2, "Admin Configuration Team", "/configuration (Team)", tab_team()),
|
||||
cfg_screen(3, "Admin Configuration Scoring", "/configuration (Scoring)", tab_scoring()),
|
||||
cfg_screen(4, "Admin Configuration Notifications", "/configuration (Notifications)", tab_notifications()),
|
||||
cfg_screen(5, "Admin Configuration Integrations", "/configuration (Integrations)", tab_integrations()),
|
||||
])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ CRITERIA
|
||||
CRITERIA = [("Problem solving", 30, 0, False), ("Code quality", 25, 1, False), ("Communication", 20, 2, False), ("System design", 15, 3, False), ("Culture fit", 10, 4, False), ("Whiteboarding", 0, 5, True)]
|
||||
CW = [None, 100, 100, 120, 260]
|
||||
|
||||
|
||||
def criteria_table(rows_=CRITERIA, state="data"):
|
||||
out = th_row(["Label", "Weight", "Position", "Status", "Actions"], CW)
|
||||
if state == "empty":
|
||||
out += div() + row("Empty Row", I("Empty / Simple", "Empty", "", Description="No data"), justify_content="center", padding="var(--spacing-xl)", align_self="stretch", background=fg("bg-elevated"))
|
||||
elif state == "loading":
|
||||
out += div() + row("Loading Row", I("Spin / Large", "Spin", ""), justify_content="center", padding="var(--spacing-xxl)", align_self="stretch", background=fg("bg-elevated"))
|
||||
for i, (l, w, p, arch) in enumerate(rows_ if state == "data" else []):
|
||||
dis = "opacity:.5"
|
||||
btns = (I("Ant Button / Default Small", "Edit", "", Label="Edit") + I("Ant Button / Default Small", "Archive", "", Label="Unarchive" if arch else "Archive") +
|
||||
I("Ant Button / Default Small", "Up", dis if i == 0 else "", Label="↑") + I("Ant Button / Default Small", "Down", dis if i == len(rows_) - 1 else "", Label="↓"))
|
||||
out += div() + td_row(f"Row / {l}", [T("Label", l, 14, fg("text-primary")), T("Weight", f"{w}%", 14, fg("text-primary")), T("Position", str(p), 14, fg("text-primary")),
|
||||
I("Ant Tag / Default" if arch else "Ant Tag / Success", "Status Tag", "", Label="Archived" if arch else "Active"), row("Actions", btns, gap="8px")], CW)
|
||||
return col("Table", out, align_self="stretch", background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-md)", overflow="hidden")
|
||||
|
||||
|
||||
def criteria_screen(name, route, state="data", overlay=""):
|
||||
c = card("Card / Criteria", T("Description", "Active criteria are shown in the interview scorecard. Archived criteria are hidden from the scorecard but retained (scores referencing them are never deleted).", 14, fg("text-secondary")) +
|
||||
criteria_table(state=state), w=1100)
|
||||
return screen(name, route, "Rubric", "Rubric", "Manage scorecard criteria — create, edit, archive, reorder", c, wide=True,
|
||||
actions=I("Ant Button / Primary", "Add Button", "", Label="Add criterion"), overlay=overlay)
|
||||
|
||||
|
||||
def modal_form(title, ok, vals):
|
||||
def f(label, value, ph=False):
|
||||
ctl = I("Input" if ph else "Input / Filled", "Input", "width:472px", Value=value)
|
||||
return col(f"Field / {label}", row("Label Row", T("Required Mark", "*", 14, fg("error"), lh=22) + T("Label", label, 14, fg("text-primary"), lh=22), gap="4px") + ctl, gap="8px")
|
||||
fields = col("Form", f("Label", vals[0], vals[0].startswith("e.g.")) + f("Weight (%)", vals[1]) + f("Position", vals[2]), gap="24px")
|
||||
header = row("Header", T("Title", title, 16, fg("text-primary"), 600, lh=24) + ic("x", 14, fg("text-secondary"), "Close Icon"), justify_content="space-between")
|
||||
footer = row("Footer", I("Ant Button / Default", "Cancel Button", "", Label="Cancel") + I("Ant Button / Primary", "OK Button", "", Label=ok), justify_content="flex-end", gap="8px")
|
||||
return col("Modal", header + fields + footer, width="520px", gap="16px", padding="24px", background=fg("bg-elevated"), border_radius="var(--radius-lg)", box_shadow="var(--shadow-lg)")
|
||||
|
||||
|
||||
def build_criteria():
|
||||
return section(SECTIONS["criteria"], "Admin / Criteria", [
|
||||
criteria_screen("Admin Criteria", "/criteria"),
|
||||
criteria_screen("Admin Criteria Empty", "/criteria (no criteria)", "empty"),
|
||||
criteria_screen("Admin Criteria Loading", "/criteria (loading)", "loading"),
|
||||
criteria_screen("Admin Criteria Add Modal", "/criteria (Add criterion modal)", overlay=modal_form("Add criterion", "Create", ("e.g. Problem solving", "0", "6"))),
|
||||
criteria_screen("Admin Criteria Edit Modal", "/criteria (Edit criterion modal)", overlay=modal_form("Edit criterion", "Save", ("Code quality", "25", "1"))),
|
||||
])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ SCHEDULE
|
||||
SESS = [("Backend Engineer · Sara Ahmed", "K7M2QX", "Open", "Blue", "2026-07-14 10:00:00"), ("Fullstack · Tomás López", "R4TP9D", "In progress", "Gold", "2026-07-14 14:30:00"),
|
||||
("SQL deep-dive · Aisha Khan", "B8WN3C", "Submitted", "Purple", "2026-07-15 09:00:00"), ("Live coding · Owen Park", "H2VZ6L", "Graded", "Green", "2026-07-16 11:00:00"),
|
||||
("Frontend Engineer · Mei Lin Tan", "Q5XJ1S", "Draft", "Gray", None), ("Platform · Dmitri Sokolov", "M9AE7F", "Cancelled", "Red", None), ("Data Engineer · Priya Iyer", "T3GY8U", "Open", "Blue", None)]
|
||||
SW = [None, 130, 140, 300]
|
||||
|
||||
|
||||
def schedule_table():
|
||||
head = row("Header Row", "".join(T(f"Col {i}", c, 12, fg("text-secondary"), 600, lh=16, letter_spacing="0.36px", **({"width": f"{w}px"} if w else {"flex_grow": "1"}))
|
||||
for i, (c, w) in enumerate(zip(["SESSION", "CODE", "STATUS", "SCHEDULED START"], SW), 1)), gap="16px", padding="12px 16px", background=fg("bg-sider"), align_self="stretch")
|
||||
out = head
|
||||
for t, code, st, tone, ts in SESS:
|
||||
chip = row("Code Chip", T("Code", code, 12, fg("text-secondary"), lh=16), padding="2px 8px", background=fg("bg-sider"), border="1px solid var(--color-border-light)", border_radius="var(--radius-sm)")
|
||||
sched = row("Schedule Cell", I("Date Picker", "Date Picker", "width:200px", Value={"color": fg("text-primary" if ts else "text-placeholder")} if False else (ts or "Select date")) +
|
||||
(I("Button / Ghost Small", "Clear Button", "", Label="Clear") if ts else ""), gap="8px")
|
||||
out += div() + td_row(f"Row / {code}", [T("Title", t, 14, fg("text-primary"), 600), chip, I(f"Tag Dot / {tone}", "Status Tag", "", Label=st), sched], SW, pad="12px 16px")
|
||||
return col("Data Table", out, align_self="stretch", background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)", overflow="hidden", box_shadow="var(--shadow-sm)")
|
||||
|
||||
|
||||
def schedule_screen(name, route, state="data"):
|
||||
search = row("Toolbar", I("Search Input", "Search Input", "width:240px", Placeholder="Search by title or code…"), justify_content="flex-end", align_self="stretch", padding="0px 0px 16px")
|
||||
if state == "data":
|
||||
body = schedule_table()
|
||||
elif state == "empty":
|
||||
body = I("Empty State", "Empty State", "width:1100px", Title="Nothing to schedule", Description="Sessions you create will appear here so you can plan when they run.")
|
||||
else:
|
||||
body = row("Loading", I("Spin / Large", "Spin", ""), justify_content="center", padding="var(--spacing-xxl)", align_self="stretch")
|
||||
return screen(name, route, "Schedule", "Schedule", "Plan when sessions run", col("Schedule", search + body, gap="0px"), wide=True)
|
||||
|
||||
|
||||
def build_schedule():
|
||||
return section(SECTIONS["schedule"], "Admin / Schedule", [
|
||||
schedule_screen("Admin Schedule", "/schedule"),
|
||||
schedule_screen("Admin Schedule Empty", "/schedule (no sessions)", "empty"),
|
||||
schedule_screen("Admin Schedule Loading", "/schedule (loading)", "loading"),
|
||||
])
|
||||
|
||||
|
||||
BUILDERS = {"auth": build_auth, "dashboard": build_dashboard, "configuration": build_configuration, "criteria": build_criteria, "schedule": build_schedule}
|
||||
|
||||
|
||||
def main():
|
||||
fid = next(i["id"] for i in call("list_resources")["items"] if i["name"] == PROJECT)
|
||||
P = pages(fid)["Admin"]
|
||||
call("import_components", {"fromPageId": pages(fid)["Design System"].id, "pageId": P.id})
|
||||
want = sys.argv[1:] or list(BUILDERS)
|
||||
# remove my old sections first (instances of my own components go with them), then rebuild components
|
||||
root = P.c("get_children", nodeId="root")["children"]
|
||||
old = [c["id"] for c in root if c["name"] in SECTIONS.values() and any(SECTIONS[k] == c["name"] for k in (want if want != ['comps'] else []))]
|
||||
if old:
|
||||
P.c("delete_nodes", nodeIds=old)
|
||||
if "comps" in want or len(sys.argv) == 1 or not [c for c in comp_specs() if c[0] in {x["name"] for x in root}]:
|
||||
build_components(P)
|
||||
top = start_top(P)
|
||||
for k in [w for w in want if w in BUILDERS]:
|
||||
html = BUILDERS[k]()
|
||||
nid, top = place(P, SECTIONS[k], html, top)
|
||||
r = P.c("get_node_info", nodeId=nid)
|
||||
print(k, nid, r.get("width"), r.get("height"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,267 @@
|
||||
"""Candidate page: `Section / Candidate Access` (/, /invite/[code], /invite/[code]/verify) and
|
||||
`Section / Candidate Session` (/session/[id], /setup, /review). build(P, y0) is called by candidate.py (for "access" and "session")."""
|
||||
from cand_common import *
|
||||
|
||||
# colours of the login split / hero / system-check page that have no token (globals.css, session page)
|
||||
VARS = {
|
||||
"color-login-sub": "#ffffffa6", "color-login-icon-bg": "#ffffff12", "color-login-icon-fg": "#ffffff8c", "color-login-desc": "#ffffff99",
|
||||
"color-login-card": "#ffffff0d", "color-login-card-line": "#ffffff14", "color-login-card-head": "#ffffff99", "color-login-row": "#ffffffc7",
|
||||
"color-login-footer": "#ffffff8c", "color-hero-chip": "#ffffff33", "color-hero-text": "#ffffffeb", "color-cam-label": "#ffffff99",
|
||||
"color-cam-hint": "#ffffff99", "color-cam-ring": "#ffffff40",
|
||||
}
|
||||
BRAND = fg("brand-primary")
|
||||
INV = fg("text-inverse")
|
||||
SUB = fg("text-secondary")
|
||||
|
||||
|
||||
def card(name, kids, pad="var(--spacing-lg)", **st):
|
||||
return col(name, kids, padding=pad, background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)",
|
||||
box_shadow="var(--shadow-sm)", **st)
|
||||
|
||||
|
||||
def page(name, body, bg="var(--color-bg-layout)", direction="row"):
|
||||
return box(name, body, display="flex", flex_direction=direction, align_items="stretch", width="1440px", height="900px", background=bg, overflow="hidden")
|
||||
|
||||
|
||||
def label(text_, req=False, right=""):
|
||||
star = T("Required Mark", "*", 14, fg("error")) if req else ""
|
||||
return row("Label Row", row("Label Group", star + T("Label", text_, 14), gap="4px") + right, justify_content="space-between", height="32px", width="100%")
|
||||
|
||||
|
||||
def field(name, label_, ctrl, req=False, right="", mb="24px"):
|
||||
return col(name, label(label_, req, right) + ctrl, width="100%", gap="0px", padding=f"0 0 {mb} 0")
|
||||
|
||||
|
||||
def brand_mark(dark=True):
|
||||
return row("Brand", row("Mark", T("Letter", "H", 14, INV, 800, lh=18), justify_content="center", width="28px", height="28px", border_radius="7px", background=BRAND) +
|
||||
T("Brand Name", "Hireloop", 16, INV if dark else fg("text-primary"), 700, lh=20), gap="12px")
|
||||
|
||||
|
||||
def alert(msg, w, icon=True):
|
||||
mw = w - 26 - (24 if icon else 0)
|
||||
return row("Alert / Error", (ic("x-circle", 16, fg("score-poor"), "Alert Icon") if icon else "") + T("Message", msg, 14, fg("text-primary"), lh=22, width=f"{mw}px"),
|
||||
gap="8px", width=f"{w}px", padding="8px 12px", background=fg("tag-red-bg"), border="1px solid var(--color-tag-red-bd)", border_radius="var(--radius-md)")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ / (login split)
|
||||
FEATURES = [("squares-four", "Team-wide visibility", "Track every candidate across the loop, from screen to offer."),
|
||||
("brackets-curly", "Live coding & system design", "Watch candidates work in a real shared editor and whiteboard."),
|
||||
("diamonds-four", "Calibrated scorecards", "A shared rubric keeps every interviewer's ratings comparable.")]
|
||||
PIPELINE = [("Amara Okafor", "Onsite loop"), ("Mei Lin Tan", "Debrief"), ("Sofia Bianchi", "Offer")]
|
||||
|
||||
|
||||
def marketing():
|
||||
feats = "".join(row(f"Feature / {t}", row("Feature Icon", ic(i, 16, fg("login-icon-fg"), "Feature Glyph"), justify_content="center", width="32px", height="32px",
|
||||
border_radius="6px", background=fg("login-icon-bg")) +
|
||||
col("Feature Text", T("Feature Title", t, 14, INV, 600, lh=18) + T("Feature Desc", d, 12, fg("login-desc"), lh=18), gap="4px", flex_grow="1", width="500px"),
|
||||
gap="16px", align_items="flex-start") for i, t, d in FEATURES)
|
||||
prow = "".join(row(f"Row / {n}", row("Name", box("Dot", width="8px", height="8px", border_radius="99px", background=fg("login-live-fg")) + T("Name", n, 14, fg("login-row"), lh=18), gap="8px") +
|
||||
T("Stage", s, 12, fg("login-desc"), lh=16), justify_content="space-between", padding="8px 0") +
|
||||
(box("Divider", width="100%", height="1px", background=fg("login-card-line")) if k < 2 else "")
|
||||
for k, (n, s) in enumerate(PIPELINE))
|
||||
pipe = col("Pipeline Card", row("Pipeline Header", T("Pipeline Title", "CANDIDATE PIPELINE", 12, fg("login-card-head"), 700, lh=16, letter_spacing="0.7px") +
|
||||
row("Live Pill", T("Live Label", "Live", 12, fg("login-live-fg"), 600, lh=16), padding="4px 8px", border_radius="99px", background=fg("login-live-bg")),
|
||||
justify_content="space-between", padding="0 0 8px 0") + prow,
|
||||
padding="16px", background=fg("login-card"), border="1px solid var(--color-login-card-line)", border_radius="12px", width="100%")
|
||||
hero = col("Hero", T("Hero Title", "Run better interviews, together.", 30, INV, 800, lh=36, letter_spacing="-0.6px") +
|
||||
T("Hero Sub", "Structured scorecards, live coding sessions, and calibrated hiring decisions — all in one workspace for your team.", 14, fg("login-sub"), lh=22), gap="16px")
|
||||
mid = col("Marketing Body", hero + col("Features", feats, gap="20px") + pipe, gap="32px", flex_grow="1", justify_content="center", padding="56px 0 0 0")
|
||||
return col("Marketing Panel", brand_mark() + mid + T("Footer", "© 2026 Hireloop. All rights reserved.", 12, fg("login-footer"), lh=16),
|
||||
width="662px", padding="32px 44px 32px 40px", background=fg("login-bg"))
|
||||
|
||||
|
||||
G_LOGO = ('<svg layer-name="Google Logo" width="16" height="16" viewBox="0 0 24 24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>'
|
||||
'<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>'
|
||||
'<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>'
|
||||
'<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>')
|
||||
M_LOGO = ('<svg layer-name="Microsoft Logo" width="16" height="16" viewBox="0 0 24 24"><rect x="1" y="1" width="10" height="10" fill="#F25022"/><rect x="13" y="1" width="10" height="10" fill="#7FBA00"/>'
|
||||
'<rect x="1" y="13" width="10" height="10" fill="#00A4EF"/><rect x="13" y="13" width="10" height="10" fill="#FFB900"/></svg>')
|
||||
|
||||
|
||||
def sso(name, logo):
|
||||
return row(f"Button / {name}", logo + T("Label", name, 16, lh=24), justify_content="center", gap="8px", height="44px", flex_grow="1", background=fg("bg-elevated"),
|
||||
border="1px solid var(--color-border)", border_radius="var(--radius-md)")
|
||||
|
||||
|
||||
def login(error=False):
|
||||
err = alert("Email/password sign-in not yet configured. Use an invite link.", 400, True) if error else ""
|
||||
err = col("Alert Wrap", err, padding="0 0 20px 0", width="100%") if error else ""
|
||||
forgot = T("Forgot", "Forgot password?", 14, BRAND)
|
||||
form = (field("Field / Work email", "Work email", I("Input / Large", "Email", style="width:100%", Value="you@company.com"), True) +
|
||||
field("Field / Password", "Password", I("Input / Password", "Password", style="width:100%", Value="" if not error else "••••••••••"), True, forgot) +
|
||||
col("Remember", I("Checkbox / Unchecked", "Remember", Label="Keep me signed in"), padding="0 0 16px 0") +
|
||||
I("Ant Button / Primary Large", "Sign in", style="width:100%", Label="Sign in"))
|
||||
divider = row("Divider", box("Line", height="1px", flex_grow="1", background=fg("border-light")) + T("Divider Text", "or continue with", 12, SUB, lh=16) +
|
||||
box("Line", height="1px", flex_grow="1", background=fg("border-light")), gap="12px", width="100%", padding="16px 0")
|
||||
links = col("Links", row("Admin Link", T("Text", "Don't have access?", 14, SUB, lh=20) + T("Link", "Ask your workspace admin for an invite", 14, BRAND, lh=20), justify_content="center", gap="4px") +
|
||||
row("Candidate Link", T("Text", "Interviewing for a role?", 14, SUB, lh=20) + T("Link", "Candidate sign in →", 14, SUB, lh=20), justify_content="center", gap="4px"),
|
||||
gap="8px", padding="24px 0 0 0", width="100%")
|
||||
inner = col("Login Form", T("Heading", "Sign in to your workspace", 30, fg("text-primary"), 700, lh=38, letter_spacing="-0.3px") +
|
||||
col("Sub Wrap", T("Sub", "Enter your credentials to continue to Hireloop.", 14, SUB, lh=22), padding="4px 0 24px 0") + err + form + divider +
|
||||
row("SSO Row", sso("Google", G_LOGO) + sso("Microsoft", M_LOGO), gap="12px", width="100%") + links, width="400px")
|
||||
right = row("Form Panel", inner, justify_content="center", flex_grow="1", background=fg("bg-elevated"), padding="48px 40px")
|
||||
return page("Screen / Login" + (" Error" if error else ""), marketing() + right)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ /invite/[code]
|
||||
def public(name, kids):
|
||||
c = col("Card", kids, width="384px", padding="24px", background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-md)")
|
||||
return page(name, row("Main", c, justify_content="center", width="100%", padding="48px"))
|
||||
|
||||
|
||||
def h3(t):
|
||||
return col("Heading Wrap", T("Heading", t, 24, fg("text-primary"), 600, lh=32), padding="0 0 12px 0")
|
||||
|
||||
|
||||
def para(t, secondary=False):
|
||||
return col("Para Wrap", T("Paragraph", t, 14, SUB if secondary else fg("text-primary"), lh=22), padding="0 0 16px 0")
|
||||
|
||||
|
||||
def invite(state):
|
||||
disabled = state != "ready"
|
||||
head = h3("Join interview")
|
||||
head += {"ready": para("Senior Frontend Engineer — technical interview"), "loading": para("Loading…", True), "error": ""}[state]
|
||||
ctrl = I("Input / Disabled" if disabled else "Input", "Email", style="width:100%", Value="")
|
||||
btn = I("Ant Button / Primary", "Send code", style="width:100%" + (";opacity:.5" if disabled else ""), Label="Send code")
|
||||
err = col("Alert Wrap", alert("Invite not found or expired", 336, True), padding="16px 0 0 0", width="100%") if state == "error" else ""
|
||||
name = {"ready": "Invite", "loading": "Invite Loading", "error": "Invite Error"}[state]
|
||||
return public(f"Screen / {name}", head + field("Field / Email", "Email", ctrl, True) + btn + err)
|
||||
|
||||
|
||||
def verify(error=False):
|
||||
err = col("Alert Wrap", alert("Invalid code", 336, False), padding="0 0 16px 0", width="100%") if error else ""
|
||||
ctrl = I("Input / Error" if error else "Input", "Code", style="width:100%", Value="482913" if error else "")
|
||||
return public("Screen / Verify" + (" Error" if error else ""), h3("Enter your code") + para("We sent a 6-digit code to aisha.khan@example.com.") + err +
|
||||
field("Field / Code", "6-digit code", ctrl, True) + I("Ant Button / Primary Large", "Verify", style="width:100%", Label="Verify"))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ /session/[id]
|
||||
def hero():
|
||||
chip = row("Up Next Chip", T("Chip Label", "UP NEXT", 12, INV, 600, lh=16), padding="4px 12px", border_radius="99px", background=fg("hero-chip"))
|
||||
top = col("Hero Head", row("Hero Meta", chip + T("Meta", "· Algorithm challenge", 12, fg("hero-text"), lh=16), gap="8px") +
|
||||
T("Hero Title", "Algorithm interview", 22, INV, 700, lh=28) + T("Hero Sub", "Build a debounced SearchBox", 14, fg("hero-text"), lh=22),
|
||||
gap="8px", padding="24px", width="100%", background_color="var(--color-tag-blue-fg)", background_image="linear-gradient(135deg, var(--color-brand-primary), var(--color-tag-blue-fg))")
|
||||
meta = "".join(col(f"Meta / {k}", T("Key", k.upper(), 12, SUB, lh=16, letter_spacing="0.5px") + T("Value", v, 14, fg("text-primary"), 600, lh=22), gap="4px")
|
||||
for k, v in [("Scheduled", "Mar 14, 2026, 10:00 AM"), ("Language", "TypeScript"), ("Editor", "Run + tests built in"), ("Format", "Coding + theory")])
|
||||
body = col("Hero Body", T("Intro", "You'll solve a live coding problem in a shared editor while your interviewer watches. Run tests as often as you like before submitting.", 14, SUB, lh=22) +
|
||||
row("Meta Row", meta, gap="20px", width="100%") +
|
||||
row("Actions", I("Button / Primary Large", "Enter session", Label="Enter session") + I("Button / Default Large", "Practice", Label="Practice problem"), gap="12px"),
|
||||
gap="20px", padding="24px", width="100%")
|
||||
return col("Card / Up next", top + body, width="574px", background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)",
|
||||
box_shadow="var(--shadow-sm)", overflow="hidden")
|
||||
|
||||
|
||||
def syscheck_card():
|
||||
return card("Card / System check", T("Title", "System check", 16, fg("text-primary"), 600, lh=24) +
|
||||
col("Desc Wrap", T("Desc", "We'll verify your setup before you start.", 12, SUB, lh=16), padding="8px 0 12px 0") +
|
||||
I("Check Item / Ready", "Browser", Label="Browser compatible") + I("Check Item / Ready", "Mic", Label="Microphone access") + I("Check Item / Ready", "Camera", Label="Camera access"),
|
||||
width="382px")
|
||||
|
||||
|
||||
def home_body(state):
|
||||
if state == "loading":
|
||||
return I("Skeleton", "Skeleton", style="width:100%")
|
||||
if state == "error":
|
||||
return I("Empty / Simple", "Empty", style="width:100%", Description="Session not found")
|
||||
greet = col("Greeting", T("Welcome", "Welcome back, Aisha", 26, fg("text-primary"), 700, lh=32, letter_spacing="-0.26px") +
|
||||
row("Who", T("Strong", "Candidate", 14, fg("text-primary"), 600, lh=22) + T("Rest", "· Session af17f53a", 14, SUB, lh=22), gap="8px"), gap="4px")
|
||||
tips = row("Tips", "".join(I(f"Tip Card / {t}", t) for t in ["Read carefully", "Think aloud", "Manage time"]), gap="16px")
|
||||
return (greet + row("Two Col", hero() + syscheck_card(), gap="24px", align_items="flex-start") + I("Progress Tracker / System check", "Progress") + tips)
|
||||
|
||||
|
||||
def home(state="ready"):
|
||||
name = {"ready": "Session Home", "loading": "Session Home Loading", "error": "Session Home Error"}[state]
|
||||
content = row("Page Center", col("Page", home_body(state), width="980px", gap="24px"), justify_content="center", align_items="flex-start", width="100%")
|
||||
return shell(f"Screen / {name}", "Home", "Aisha Khan", "Session af17f53a", content)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ /session/[id]/setup
|
||||
CHECKS = [("Browser compatibility", "Chrome, Edge, or Firefox — up to date"), ("Camera access", "Required for onsite & behavioral rounds"),
|
||||
("Microphone access", "Required so interviewers can hear you"), ("Network speed", "At least 5 Mbps recommended")]
|
||||
|
||||
|
||||
def setup_screen(name, states, mic):
|
||||
hdr = row("Header", brand_mark(False) + row("Steps", I("Syscheck Step / Done", "Step 1") + box("Sep", width="24px", height="1px", background=fg("border")) + I("Syscheck Step / Active", "Step 2"), gap="12px"),
|
||||
justify_content="space-between", width="100%", height="56px", padding="0 32px", background=fg("bg-elevated")) + box("Header Line", width="100%", height="1px", background=fg("border-light"))
|
||||
cam = col("Camera Preview", T("Preview Label", "preview", 12, fg("cam-label"), lh=16, position="absolute", left="12px", top="10px") +
|
||||
col("Placeholder", row("Cam Icon", ic("camera", 18, fg("cam-hint"), "Camera Off"), justify_content="center", width="36px", height="36px", border_radius="99px", border="2px solid var(--color-cam-ring)") +
|
||||
T("Hint", "camera preview", 12, fg("cam-hint"), lh=16), align_items="center", gap="8px"),
|
||||
position="relative", align_items="center", justify_content="center", width="300px", height="225px", border_radius="var(--radius-md)", background=fg("syscheck-camera"), overflow="hidden")
|
||||
miclevel = row("Mic Row", T("Mic Label", "Mic level", 12, SUB, lh=16) + row("Mic Bar", box("Mic Fill", width=f"{int(2.2 * mic)}px", height="6px", border_radius="99px", background=fg("syscheck-mic")),
|
||||
flex_grow="1", height="6px", border_radius="99px", background=fg("border-light"), overflow="hidden"), gap="12px", width="100%")
|
||||
cam_col = col("Camera Col", cam + miclevel, width="340px", align_self="stretch", padding="20px", gap="16px")
|
||||
rows = "".join(I(f"Syscheck Row / {s}", f"Check / {t}", Title=t, Desc=d) for s, (t, d) in zip(states, CHECKS))
|
||||
checks = col("Checks Col", rows, width="400px", padding="20px", gap="0px")
|
||||
sc = row("Check Card", cam_col + box("Col Line", width="1px", align_self="stretch", background=fg("border-light")) + checks, width="740px", background=fg("bg-elevated"), border_radius="var(--radius-lg)", box_shadow="var(--shadow-sm)", overflow="hidden", align_items="stretch")
|
||||
back = row("Back Button", T("Label", "Back", 14, fg("text-primary"), lh=22), padding="8px 24px", background=fg("bg-elevated"), border="1px solid var(--color-border)", border_radius="6px")
|
||||
main = col("Main", T("Title", "Let's check your setup", 22, fg("text-primary"), 700, lh=28, text_align="center") +
|
||||
col("Sub Wrap", T("Sub", "We'll verify your browser, camera, microphone, and connection before your interview begins.", 14, SUB, lh=22, text_align="center", width="480px"), padding="8px 0 28px 0") +
|
||||
sc + col("Back Wrap", back, padding="28px 0 0 0"), align_items="center", justify_content="center", flex_grow="1", padding="40px 24px")
|
||||
return page(name, hdr + main, "var(--color-syscheck-bg)", "column")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ /session/[id]/review
|
||||
ROWS = [("3f2a9c1e-7b4d-4e0a-9c55-1d8e2b6f40a7", "Graded", "", "2026-03-14T10:42:17Z"),
|
||||
("a91c5d02-34be-4f7a-8e11-6c2d90b3e5f4", "Errored", "Runner timed out after 30s", "2026-03-14T10:58:03Z"),
|
||||
("c7e48b36-0d15-42a9-b3f8-95a1e7d20c68", "Running", "", "2026-03-14T11:05:49Z")]
|
||||
|
||||
|
||||
LINE = '<tr layer-name="Divider"><td style="padding:0;height:1px;background:var(--color-border-light)"></td></tr>'
|
||||
|
||||
|
||||
def cell(tag, inner, w=None, **extra):
|
||||
st = dict(padding="16px", border="none")
|
||||
if w:
|
||||
st["width"] = w
|
||||
st.update(extra)
|
||||
return f'<{tag} style="{css(**st)}">{inner}</{tag}>'
|
||||
|
||||
|
||||
def review(state="ready"):
|
||||
hd = dict(font_size="var(--font-size-sm)", font_weight="600", color="var(--color-text-primary)", background_color="var(--color-bg-sider)")
|
||||
head = '<tr layer-name="Row / Header">' + cell("th", "Challenge", **hd) + cell("th", "Status", "160px", **hd) + cell("th", "Error", "240px", **hd) + cell("th", "Submitted at", "200px", **hd) + "</tr>" + LINE
|
||||
body = ""
|
||||
if state == "ready":
|
||||
for cid, st, e, at in ROWS:
|
||||
err = T("Error", e, 14, fg("error"), lh=22) if e else T("Error", "—", 14, lh=22)
|
||||
body += (f'<tr layer-name="Row / {cid[:8]}">' + cell("td", T("Challenge Id", cid, 14, lh=22)) + cell("td", I(f"Submission Status Badge / {st}", "Status"), "160px") +
|
||||
cell("td", err, "240px") + cell("td", T("Submitted", at, 14, lh=22), "200px") + "</tr>" + LINE)
|
||||
tbl = f'<table layer-name="Table" style="width:100%">{head}{body}</table>'
|
||||
if state == "empty":
|
||||
tbl += col("Empty Wrap", I("Empty / Simple", "Empty", style="width:100%", Description="No data"), width="100%", padding="24px 0")
|
||||
pager = row("Pagination", row("Page 1", T("Page Number", "1", 14, BRAND, lh=22), justify_content="center", width="32px", height="32px", border="1px solid var(--color-brand-primary)", border_radius="var(--radius-md)"),
|
||||
justify_content="flex-end", width="100%", padding="16px 0 0 0") if state == "ready" else ""
|
||||
err = col("Alert Wrap", alert("Failed to load submissions: network request failed", 1094, True), padding="0 0 16px 0", width="100%") if state == "error" else ""
|
||||
headbar = row("Card Head", T("Title", "Review", 24, fg("text-primary"), 600, lh=32) + T("Back Link", "Back to session", 14, BRAND, lh=22), justify_content="space-between", width="100%",
|
||||
padding="16px 24px") + box("Head Line", width="100%", height="1px", background=fg("border-light"))
|
||||
inner = col("Card Body", err + col("Table Wrap", tbl + pager, width="100%"), padding="24px", width="100%")
|
||||
c = col("Card", headbar + inner, width="100%", background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-md)")
|
||||
name = {"ready": "Review", "empty": "Review Empty", "error": "Review Error"}[state]
|
||||
return shell(f"Screen / {name}", "", "Review", "Your submissions", c)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ sections
|
||||
def section(P, name, title, screens, y, per_row=4):
|
||||
blocks = [col(f"Block / {c}", T("Caption", f"{c} · {r}", 14, SUB, 600) + h, gap="8px") for c, r, h in screens]
|
||||
rows = "".join(row(f"Screens Row {i // per_row + 1}", "".join(blocks[i:i + per_row]), gap="var(--spacing-xl)", align_items="flex-start") for i in range(0, len(blocks), per_row))
|
||||
sec = col(name, T("Section Title", title, 24, weight=700) + col("Screens", rows, gap="var(--spacing-xl)"), gap="var(--spacing-xl)", padding="var(--spacing-xl)", background="transparent")
|
||||
nid = write(P, "root", sec)[0]
|
||||
place(P, nid, 0, y)
|
||||
return nid
|
||||
|
||||
|
||||
def build(P, y0):
|
||||
call("set_variables", {"fileId": P.file, "variables": VARS})
|
||||
print("deleted", delete_named(P, lambda n: n in ("Section / Candidate Access", "Section / Candidate Session")))
|
||||
a = section(P, "Section / Candidate Access", "Candidate / Access", [
|
||||
("Login", "/", login()), ("Login error", "/", login(True)),
|
||||
("Invite", "/invite/[code]", invite("ready")), ("Invite loading", "/invite/[code]", invite("loading")),
|
||||
("Invite error", "/invite/[code]", invite("error")), ("Verify", "/invite/[code]/verify", verify()), ("Verify error", "/invite/[code]/verify", verify(True))], y0)
|
||||
h = next(c["height"] for c in top_children(P) if c["id"] == a)
|
||||
s = section(P, "Section / Candidate Session", "Candidate / Session", [
|
||||
("Session home", "/session/[id]", home()), ("Session home loading", "/session/[id]", home("loading")), ("Session home error", "/session/[id]", home("error")),
|
||||
("System check ready", "/session/[id]/setup", setup_screen("Screen / System Check Ready", ["Ready"] * 4, 60)),
|
||||
("System check checking", "/session/[id]/setup", setup_screen("Screen / System Check Checking", ["Ready", "Checking", "Checking", "Pending"], 0)),
|
||||
("System check failed", "/session/[id]/setup", setup_screen("Screen / System Check Failed", ["Ready", "Failed", "Failed", "Failed"], 0)),
|
||||
("Review", "/session/[id]/review", review()), ("Review empty", "/session/[id]/review", review("empty")), ("Review error", "/session/[id]/review", review("error"))], int(y0 + h + 200))
|
||||
return {"access": a, "session": s}
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Section / Candidate Challenge: /session/[id]/challenge/[challengeId] (IDE, theory phase, submit form). build(P, y0) -> section node id."""
|
||||
import cand_components as cc
|
||||
from cand_common import *
|
||||
|
||||
D = cc.D
|
||||
IDE_BG = "var(--color-code-bg)" # #1e1e1e
|
||||
CENTER_W = 666 # code tab centre column (1192 - 210 - 316)
|
||||
WIDE_W = 876 # diagrams / whiteboard centre column (1192 - 316)
|
||||
BODY_H = 848
|
||||
LOAD = "Loading challenge"
|
||||
|
||||
|
||||
def frame(name, content):
|
||||
"""Candidate shell, full bleed: sidebar + dark IDE area."""
|
||||
sb = I("Candidate Sidebar / My interview Active", "Sidebar", style="height:900px")
|
||||
return sb + col("Main", content, width=f"{MAIN_W}px", height="900px", background=IDE_BG)
|
||||
|
||||
|
||||
def scr(name, content, **st):
|
||||
return screen(name, frame(name, content), position="relative", **st)
|
||||
|
||||
|
||||
def rail(collapsed=False):
|
||||
return I("Problem Rail / Collapsed" if collapsed else "Problem Rail / Brief", "Problem Rail")
|
||||
|
||||
|
||||
def code_screen(name, console, toolbar="IDE Toolbar / Code", toast=False, file="SearchBox", extra=""):
|
||||
src = {"SearchBox": ("Code Editor / SearchBox", "Editor Tabs / SearchBox Active", "File Tree / SearchBox Active"),
|
||||
"Debounce": ("Code Editor / useDebounce", "Editor Tabs / Debounce Active", "File Tree / Test Active")}[file]
|
||||
centre = col("Editor Column", I(src[1], "Editor Tabs") + I(src[0], "Code Editor") + box("Resize Handle", width="100%", height="6px", background=D("deep")) + I(console, "Console"),
|
||||
width=f"{CENTER_W}px", height=f"{BODY_H}px")
|
||||
body = row("Body", I(src[2], "File Tree") + centre + rail(), height=f"{BODY_H}px", align_items="stretch")
|
||||
toast_html = I("Message / Success", "Toast", style="position:absolute;left:626px;top:16px") if toast else ""
|
||||
return scr(name, I(toolbar, "Toolbar") + body + toast_html + extra)
|
||||
|
||||
|
||||
# --- diagrams
|
||||
MERMAID = ["graph TD", " Client -->|HTTPS| LB[Load Balancer]", " LB --> API1[API Server 1]", " LB --> API2[API Server 2]", " API1 <--> Cache[(Redis Cache)]",
|
||||
" API2 <--> Cache", " API1 --> DB[(PostgreSQL Primary)]", " API2 --> DB", " DB -.->|replication| Replica[(Read Replica)]",
|
||||
" API1 --> Queue([Message Queue])", " Queue --> Worker[Background Worker]"]
|
||||
|
||||
|
||||
def dnode(label):
|
||||
return row(f"Node / {label}", T("Label", label, 12, D("text"), lh=16), padding="8px 16px", background=D("panel"), border=f"1px solid {D('text-faint')}", border_radius="var(--radius-sm)")
|
||||
|
||||
|
||||
def darrow(label=""):
|
||||
return row("Edge", ic("arrow-down", 14, D("text-muted"), "Edge Icon") + (T("Edge Label", label, 12, D("text-muted"), lh=16) if label else ""), gap="4px")
|
||||
|
||||
|
||||
def diagram_body(tpl="System design"):
|
||||
toolbar = I(f"Diagram Toolbar / {tpl}", "Diagram Toolbar", style=f"width:{WIDE_W}px")
|
||||
src = "".join(T(f"Src {i + 1}", l.replace(" ", " "), 13, fg("code-fg"), lh=23) for i, l in enumerate(MERMAID))
|
||||
left = col("Mermaid Source", T("Pane Label", "MERMAID", 12, D("text-muted"), letter_spacing="0.6px", position="absolute", right="10px", top="8px") + src,
|
||||
width="50%", height="100%", padding="16px", background=D("input"), border_right=f"1px solid {D('deep')}", position="relative", gap="0px")
|
||||
graph = col("Rendered Graph", dnode("Client") + darrow("HTTPS") + dnode("Load Balancer") + darrow() +
|
||||
row("API Row", dnode("API Server 1") + dnode("API Server 2"), gap="32px") + darrow() +
|
||||
row("Store Row", dnode("Redis Cache") + dnode("PostgreSQL Primary") + dnode("Message Queue"), gap="16px") + darrow("replication") +
|
||||
row("Leaf Row", dnode("Read Replica") + dnode("Background Worker"), gap="32px"), align_items="center", gap="4px")
|
||||
right = row("Rendered Preview", graph, width="50%", height="100%", justify_content="center", align_items="flex-start", padding="28px 20px", background=fg("diagram-bg"))
|
||||
panes = row("Panes", left + right, width=f"{WIDE_W}px", height=f"{BODY_H - 41}px", align_items="stretch")
|
||||
return row("Body", col("Diagram Editor", toolbar + panes, width=f"{WIDE_W}px", height=f"{BODY_H}px") + rail(), height=f"{BODY_H}px", align_items="stretch")
|
||||
|
||||
|
||||
# --- whiteboard
|
||||
def shape(name, x, y, w, h, color, label, r=4):
|
||||
return box(f"Shape / {name}", T("Text", label, 14, color, text_align="center", width=f"{w}px"), position="absolute", left=f"{x}px", top=f"{y}px", width=f"{w}px", height=f"{h}px",
|
||||
border=f"2px solid {color}", border_radius=f"{r}px", display="flex", align_items="center", justify_content="center")
|
||||
|
||||
|
||||
def whiteboard_body(tool="Rectangle", selected=False):
|
||||
W, B, G, R, O = fg("wb-white"), fg("wb-blue"), fg("ide-pass"), fg("ide-fail"), fg("wb-orange")
|
||||
ar = lambda n, i, c, x, y: ic(i, 40, c, n).replace("<x-paper-icon", f'<x-paper-icon style="position:absolute;left:{x}px;top:{y}px"')
|
||||
shapes = (shape("Client", 60, 70, 140, 64, W, "Client") + shape("API", 330, 70, 160, 64, B, "API Gateway") + shape("Auth", 330, 250, 160, 64, G, "Auth Service") +
|
||||
shape("DB", 620, 70, 160, 64, O, "Postgres", 32) + shape("Cache", 620, 250, 160, 64, R, "Redis", 32) +
|
||||
ar("Arrow / Client to API", "arrow-right", W, 220, 82) + ar("Arrow / API to DB", "arrow-right", B, 510, 82) + ar("Arrow / API to Auth", "arrow-down", B, 390, 160) +
|
||||
ar("Arrow / Auth to Cache", "arrow-right", G, 510, 262) + T("Note", "cache sessions, TTL 15m", 15, fg("wb-purple"), position="absolute", left="610px", top="340px"))
|
||||
canvas = box("Canvas", shapes, position="relative", width=f"{WIDE_W}px", height=f"{BODY_H - 51}px", background=fg("wb-bg"), overflow="hidden")
|
||||
tb = I(f"Whiteboard Toolbar / {'Select' if selected else tool}", "Whiteboard Toolbar", style=f"width:{WIDE_W}px")
|
||||
return row("Body", col("Whiteboard", tb + canvas, width=f"{WIDE_W}px", height=f"{BODY_H}px") + rail(), height=f"{BODY_H}px", align_items="stretch")
|
||||
|
||||
|
||||
# --- theory phase
|
||||
Q = ["Tell me about a time you disagreed with a teammate on a technical decision. How did you resolve it?",
|
||||
"How do you prioritise when three stakeholders each say their request is urgent?"]
|
||||
|
||||
|
||||
def theory_body(q, cat, diff, qn, n, answer, words, last=False):
|
||||
chips = row("Chips", I(f"Theory Chip / {cat}", "Category") + I(f"Theory Chip / {diff}", "Difficulty") + box("Spacer", flex_grow="1") +
|
||||
T("Counter", f"Question {qn} of {n}", 12, D("text-muted"), lh=16), gap="8px", width="100%")
|
||||
prev = I("Theory Button / Prev Disabled" if qn == 1 else "Theory Button / Prev", "Prev")
|
||||
left = row("Left", prev + (T("Word Count", f"{words} words", 12, D("text-muted"), lh=16) if words else ""), gap="8px")
|
||||
if last:
|
||||
right = col("Submit Group", I("Theory Button / Submit Partial", "Submit", Label="Submit (3/5 answered)") +
|
||||
T("Hint", "Unanswered questions will be marked as skipped.", 12, D("text-muted"), lh=16), align_items="flex-end", gap="8px")
|
||||
else:
|
||||
right = I("Theory Button / Next", "Next")
|
||||
page = col("Question Page", chips + col("Question Wrap", T("Question", q, 19, fg("text-inverse"), 600, lh=28, width="704px"), padding="20px 0 28px") +
|
||||
I("Theory Answer / Filled" if answer else "Theory Answer / Empty", "Answer") +
|
||||
row("Footer", left + right, justify_content="space-between", width="704px", padding="16px 0 28px"), width="760px", padding="40px 28px 0", align_items="flex-start")
|
||||
return row("Body", page, justify_content="center", align_items="flex-start", width="100%", flex_grow="1")
|
||||
|
||||
|
||||
# --- states and form
|
||||
def skeleton():
|
||||
bar = lambda i, w: box(f"Skeleton Bar {i}", width=f"{w}px", height="16px", border_radius="var(--radius-sm)", background=D("line"))
|
||||
return col("Loading", bar(1, 320) + bar(2, 880) + bar(3, 760) + bar(4, 840) + bar(5, 560) + bar(6, 880) + bar(7, 640) + bar(8, 420), padding="24px", gap="16px", width="100%", height="100%")
|
||||
|
||||
|
||||
def error_state():
|
||||
return col("Error", T("Error Message", "Failed to load challenge", 14, fg("ide-fail")), padding="24px", width="100%", height="100%")
|
||||
|
||||
|
||||
def form_body(inner):
|
||||
return col("Form Page", inner, padding="24px", width=f"{MAIN_W}px", height="100%", background=fg("bg-layout"), gap="16px")
|
||||
|
||||
|
||||
def form_screen(name, inner):
|
||||
sb = I("Candidate Sidebar / My interview Active", "Sidebar", style="height:900px")
|
||||
return screen(name, sb + col("Main", form_body(inner), width=f"{MAIN_W}px", height="900px", background=fg("bg-layout")))
|
||||
|
||||
|
||||
def mcq_form(error=None):
|
||||
qs = [("1. Which HTTP status code means the request succeeded and created a resource?", ["200 OK", "201 Created", "204 No Content", "302 Found"], 1),
|
||||
("2. What does the CAP theorem say about a network partition?", ["You must choose consistency or availability", "You can have all three guarantees",
|
||||
"Partitions never happen in practice", "Only availability is affected"], 0)]
|
||||
items = ""
|
||||
for qi, (q, opts, sel) in enumerate(qs, 1):
|
||||
rad = "".join(I("Radio / Selected" if i == sel else "Radio / Unselected", f"Option / {o}", Label=o) for i, o in enumerate(opts))
|
||||
items += col(f"Form Item / Q{qi}", T("Label", q, 14, fg("text-primary"), lh=32) + col("Options", rad, gap="8px"), gap="4px", padding="0 0 24px")
|
||||
err = row("Alert", ic("x-circle", 16, fg("score-poor"), "Alert Icon") + T("Message", error, 14, fg("text-primary"), flex_grow="1"), gap="8px", width="720px", padding="8px 12px",
|
||||
background=fg("tag-red-bg"), border="1px solid var(--color-tag-red-bd)", border_radius="var(--radius-md)") if error else ""
|
||||
return col("ChallengeSubmitForm", err + col("Form", items + I("Ant Button / Primary", "Submit Button", Label="Submit"), gap="0px"), width="720px", gap="16px")
|
||||
|
||||
|
||||
def text_form():
|
||||
return col("ChallengeSubmitForm", col("Form Item", T("Label", "Your answer", 14, fg("text-primary"), lh=32) +
|
||||
row("Textarea", T("Value", "I would split the monolith along its billing and catalogue boundaries first, because those two teams already deploy independently.", 14, fg("text-primary"), flex_grow="1"),
|
||||
align_items="flex-start", width="720px", height="260px", padding="4px 12px", background=fg("bg-elevated"), border="1px solid var(--color-border)", border_radius="var(--radius-md)"),
|
||||
gap="0px", padding="0 0 24px") + I("Ant Button / Primary", "Submit Button", Label="Submit"), width="720px")
|
||||
|
||||
|
||||
def status_strip():
|
||||
tags = "".join(I(f"Submission Status Badge / {s}", f"Badge / {s}") for s in ["Queued", "Running", "Graded", "Errored", "Unknown"])
|
||||
return col("SubmissionStatusBadge", T("Title", "Submission status", 16, weight=600) + row("Badges", tags, gap="8px"), padding="var(--spacing-lg)", gap="12px",
|
||||
background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)", width="720px")
|
||||
|
||||
|
||||
def build(P, y0):
|
||||
name = "Section / Candidate Challenge"
|
||||
print("deleted", delete_named(P, lambda n: n == name))
|
||||
code_idle = code_screen("Screen / Challenge Code Idle", "Console / Empty")
|
||||
sets = [
|
||||
("Code: idle", "/session/[id]/challenge/[challengeId]", code_idle),
|
||||
("Code: running tests", "state: Run clicked, console streaming", code_screen("Screen / Challenge Code Running", "Console / Running", file="Debounce")),
|
||||
("Code: tests passed", "state: Run finished", code_screen("Screen / Challenge Code Passed", "Console / Passed")),
|
||||
("Code: tests failed, low time", "state: timer under 5 minutes", code_screen("Screen / Challenge Code Failed", "Console / Failed", "IDE Toolbar / Code Low Time")),
|
||||
("Code: submitted", "state: Submit clicked, then redirect to /session/[id]/review", code_screen("Screen / Challenge Code Submitted", "Console / Passed", toast=True)),
|
||||
("Diagrams tab", "tab: Diagrams", scr("Screen / Challenge Diagrams", I("IDE Toolbar / Diagrams", "Toolbar") + diagram_body())),
|
||||
("Whiteboard tab", "tab: Whiteboard", scr("Screen / Challenge Whiteboard", I("IDE Toolbar / Whiteboard", "Toolbar") + whiteboard_body())),
|
||||
("Whiteboard: shape selected", "tool: Select", scr("Screen / Challenge Whiteboard Selected", I("IDE Toolbar / Whiteboard", "Toolbar") + whiteboard_body(selected=True))),
|
||||
("Theory: answered", "type: THEORY", scr("Screen / Challenge Theory", I("Theory Toolbar / Q2 of 5", "Toolbar") +
|
||||
theory_body(Q[0], "Category", "Medium", 2, 5, True, 38))),
|
||||
("Theory: last question", "type: THEORY, submit", scr("Screen / Challenge Theory Submit", I("Theory Toolbar / Q5 of 5", "Toolbar") +
|
||||
theory_body(Q[1], "Category", "Hard", 5, 5, False, 0, last=True))),
|
||||
("Loading", "state: loading", scr("Screen / Challenge Loading", skeleton())),
|
||||
("Load error", "state: error", scr("Screen / Challenge Error", error_state())),
|
||||
("MCQ form", "type: MCQ (ChallengeSubmitForm)", form_screen("Screen / Challenge MCQ", mcq_form())),
|
||||
("MCQ form: submit error", "state: submit failed", form_screen("Screen / Challenge MCQ Error", mcq_form("Submit failed"))),
|
||||
("Text form + status badges", "type: text (ChallengeSubmitForm), SubmissionStatusBadge", form_screen("Screen / Challenge Text", text_form() + status_strip())),
|
||||
]
|
||||
rows = ""
|
||||
for i in range(0, len(sets), 2):
|
||||
blocks = "".join(col(f"Block / {c}", T("Caption", f"{c} · {r}", 14, fg("text-secondary"), 600) + html, gap="8px") for c, r, html in sets[i:i + 2])
|
||||
rows += row(f"Row {i // 2 + 1}", blocks, gap="var(--spacing-xl)", align_items="flex-start")
|
||||
sec = col(name, T("Section Title", "Candidate Challenge", 24, weight=700) + col("Screens", rows, gap="var(--spacing-xl)"), gap="var(--spacing-xl)",
|
||||
padding="var(--spacing-xl)", background="transparent")
|
||||
nid = write(P, "root", sec)[0]
|
||||
place(P, nid, 0, y0)
|
||||
return nid
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fid, P, DS = setup_cand()
|
||||
y0 = int(max(c.get("y", 0) + c["height"] for c in top_children(P) if not c["name"].startswith("Section / Candidate"))) + 200
|
||||
print(build(P, y0))
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Screenshot each Candidate Challenge screen: python3 cand_challenge_shots.py [filter]"""
|
||||
import sys
|
||||
from cand_common import *
|
||||
fid, P, DS = setup_cand()
|
||||
def walk(nid, out):
|
||||
for c in P.c("get_children", nodeId=nid)["children"]:
|
||||
if c["name"].startswith("Screen / Challenge"): out[c["name"]] = c["id"]
|
||||
elif c["name"].startswith(("Section /", "Screens", "Row ", "Block /")): walk(c["id"], out)
|
||||
return out
|
||||
sec = next(c["id"] for c in top_children(P) if c["name"] == "Section / Candidate Challenge")
|
||||
for n, i in walk(sec, {}).items():
|
||||
if len(sys.argv) < 2 or sys.argv[1] in n:
|
||||
print(shot(P, i, n.replace(" / ", "_").replace(" ", "_")))
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Shared helpers for the Candidate page scripts (candidate.py runs cand_components, cand_access, cand_session, cand_challenge)."""
|
||||
import json, sys
|
||||
|
||||
sys.path.insert(0, "/Volumes/Storage/Projects/papercraft/examples/interview")
|
||||
from ds_common import * # noqa: F401,F403 (pc helpers, T, ic, PROJECT)
|
||||
|
||||
OUT = "/private/tmp/claude-501/-Volumes-Storage-Projects-photocraft/11f4040a-b4f4-4b9e-965a-bde08b400450/scratchpad/"
|
||||
SIDEBAR_W, MAIN_W = 248, 1192
|
||||
|
||||
# Colors that exist in the candidate app code but have no token in tokens.css (IDE dark theme, login split, system check).
|
||||
CAND_VARS = {
|
||||
# IDE dark theme (apps/web/src/components/ide/*)
|
||||
"color-ide-panel": "#252526", "color-ide-deep": "#1a1a1a", "color-ide-console": "#181818", "color-ide-input": "#161616",
|
||||
"color-ide-line": "#2a2a2a", "color-ide-active": "#2a2d2e", "color-ide-rail": "#1f1f1f",
|
||||
"color-ide-text": "#cccccc", "color-ide-text-muted": "#888888", "color-ide-text-faint": "#666666", "color-ide-text-dim": "#bbbbbb",
|
||||
"color-ide-pass": "#3fb950", "color-ide-fail": "#f85149", "color-ide-warn": "#d29922",
|
||||
"color-ide-pass-bg": "#1a3a1a", "color-ide-warn-bg": "#2a2a14", "color-ide-fail-bg": "#3a1a1a", "color-ide-teal-bg": "#1a3540", "color-ide-blue-bg": "#1a2a3a",
|
||||
"color-ide-submit-bg": "#1c2a40", "color-ide-submit-fg": "#5a80a8",
|
||||
"color-syntax-comment": "#6a9955", "color-syntax-string": "#ce9178", "color-syntax-keyword": "#569cd6", "color-syntax-type": "#4ec9b0",
|
||||
"color-syntax-number": "#b5cea8", "color-syntax-fn": "#dcdcaa", "color-syntax-gutter": "#5a5a5a",
|
||||
"color-wb-bg": "#13131f", "color-wb-toolbar": "#1c1c2e", "color-wb-border": "#252540", "color-wb-button": "#252540", "color-wb-text": "#9999bb",
|
||||
"color-diagram-bg": "#0d1117", "color-ide-css": "#c586c0", "color-ide-pass-border": "#3fb95035", "color-ide-chip": "#252525", "color-ide-sep": "#333333",
|
||||
"color-wb-white": "#e0e0e0", "color-wb-blue": "#6ab0ff", "color-wb-purple": "#c792ea", "color-wb-orange": "#ff9f43", "color-ide-fail-border": "#5a2020",
|
||||
# login split (globals.css .login-*)
|
||||
"color-login-bg": "#0f1117", "color-login-live-bg": "#16a34a2e", "color-login-live-fg": "#4ade80",
|
||||
# system check page (globals.css .syscheck-*)
|
||||
"color-syscheck-bg": "#f0f0f0", "color-syscheck-ready": "#16a34a", "color-syscheck-ready-bg": "#16a34a1a", "color-syscheck-fail": "#dc2626",
|
||||
"color-syscheck-camera": "#111111", "color-syscheck-mic": "#22c55e",
|
||||
}
|
||||
|
||||
|
||||
def fg(c):
|
||||
return f"var(--color-{c})"
|
||||
|
||||
|
||||
def setup_cand():
|
||||
fid = next(i["id"] for i in call("list_resources")["items"] if i["name"] == PROJECT)
|
||||
ps = pages(fid)
|
||||
call("set_variables", {"fileId": fid, "variables": CAND_VARS})
|
||||
return fid, ps["Candidate"], ps["Design System"]
|
||||
|
||||
|
||||
def I(comp, name=None, style=None, **ov):
|
||||
"""Instance html: I("Button / Primary", Label="Save", style="width:100%")."""
|
||||
a = f' layer-name="{name}"' if name else ""
|
||||
s = f' style="{style}"' if style else ""
|
||||
o = json.dumps(ov, ensure_ascii=False).replace("'", "'")
|
||||
return f"<x-paper-instance component=\"{comp}\"{a}{s} overrides='{o}'/>"
|
||||
|
||||
|
||||
def write(P, parent, html):
|
||||
r = P.c("write_html", targetNodeId=parent, mode="insert-children", html=html)
|
||||
for w in r.get("warnings", []):
|
||||
print(" warn:", w)
|
||||
if r.get("ignoredStyles"):
|
||||
print(" ignored:", sorted(set(map(str, r["ignoredStyles"])))[:8])
|
||||
return r.get("createdNodeIds", [])
|
||||
|
||||
|
||||
def top_children(P):
|
||||
return P.c("get_children", nodeId="root")["children"]
|
||||
|
||||
|
||||
def delete_named(P, pred):
|
||||
ids = [c["id"] for c in top_children(P) if pred(c["name"])]
|
||||
if ids:
|
||||
P.c("delete_nodes", nodeIds=ids)
|
||||
return len(ids)
|
||||
|
||||
|
||||
def place(P, node, x, y):
|
||||
P.c("update_styles", updates=[{"nodeIds": [node], "styles": {"left": x, "top": y}}])
|
||||
|
||||
|
||||
def make_section(P, name, title, screens, x, y):
|
||||
"""screens: [(caption, route, screen html)] -> one `Section / ...` frame holding captioned screens in a row."""
|
||||
blocks = "".join(col(f"Block / {c}", T("Caption", f"{c} · {r}", 14, fg("text-secondary"), 600) + html, gap="8px") for c, r, html in screens)
|
||||
sec = col(name, T("Section Title", title, 24, weight=700) + row("Screens", blocks, gap="var(--spacing-xl)", align_items="flex-start"),
|
||||
gap="var(--spacing-xl)", padding="var(--spacing-xl)", background="transparent")
|
||||
nid = write(P, "root", sec)[0]
|
||||
place(P, nid, x, y)
|
||||
return nid
|
||||
|
||||
|
||||
def screen(name, body, w=1440, h=900, **st):
|
||||
return row(name, body, width=f"{w}px", height=f"{h}px", background="var(--color-bg-layout)", overflow="hidden", align_items="stretch", **st)
|
||||
|
||||
|
||||
def shell(name, active, title, subtitle, content, sidebar_name=None):
|
||||
"""Candidate shell screen: sidebar instance + header instance + padded content."""
|
||||
sb = I(f"Candidate Sidebar / {active} Active", "Sidebar", style="height:900px") if active else I("Candidate Sidebar", "Sidebar")
|
||||
main = col("Main", I("Candidate Shell Header", "Header", style="height:134px", Title=title, Subtitle=subtitle) +
|
||||
col("Content", content, padding="var(--page-padding)", gap="var(--spacing-lg)", flex_grow="1", overflow="hidden"),
|
||||
width=f"{MAIN_W}px", background="var(--color-bg-layout)")
|
||||
return screen(name, sb + main)
|
||||
|
||||
|
||||
def shot(P, node, name, scale=1):
|
||||
P.screenshot(OUT + name + ".png", node, scale)
|
||||
return OUT + name + ".png"
|
||||
@@ -0,0 +1,370 @@
|
||||
"""Main components of the Candidate page (IDE pieces, system-check rows, session cards). Names: `Group / Variant`."""
|
||||
import html as _h
|
||||
import re
|
||||
|
||||
from cand_common import *
|
||||
|
||||
SPECS, GROUPS, NOTES = [], {}, {}
|
||||
|
||||
|
||||
def reg(group, name, html, note=""):
|
||||
SPECS.append((name, html))
|
||||
GROUPS.setdefault(group, []).append(name)
|
||||
NOTES[name] = note
|
||||
|
||||
|
||||
def D(c):
|
||||
return f"var(--color-ide-{c})"
|
||||
|
||||
|
||||
BRAND = fg("brand-primary")
|
||||
TOOLBAR_W = MAIN_W
|
||||
CENTER_W = 666 # 1192 - 210 file tree - 316 rail
|
||||
BODY_H = 848 # 900 - 52 toolbar
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ IDE toolbar
|
||||
def tb_tab(icon, label, active):
|
||||
c = fg("text-inverse") if active else D("text-muted")
|
||||
return col(f"Tab / {label}", row("Tab Body", ic(icon, 14, c, "Tab Icon") + T("Label", label, 14, c, 600 if active else 400), height="50px", padding="0 18px", gap="8px") +
|
||||
box("Ink Bar", height="2px", background=BRAND if active else "transparent"))
|
||||
|
||||
|
||||
def timer(txt="44:12", low=False):
|
||||
c = D("fail") if low else D("text")
|
||||
return row("Timer Pill", ic("clock", 14, c, "Timer Icon") + T("Timer", txt, 14, c, 700), gap="8px", padding="5px 12px", background=D("deep"), border_radius="6px",
|
||||
border=f"1px solid {D('fail-border') if low else D('line')}")
|
||||
|
||||
|
||||
def toolbar(name, active, low=False, timer_txt="44:12"):
|
||||
left = (row("Exit", ic("arrow-left", 14, D("text-faint"), "Exit Icon") + T("Exit Label", "Exit", 14, D("text-faint")), gap="6px", padding="0 12px 0 0") +
|
||||
box("Divider", width="1px", height="20px", background=D("sep")) + box("Spacer", width="16px"))
|
||||
tabs = "".join(tb_tab(i, l, l == active) for i, l in [("code", "Code"), ("flow-arrow", "Diagrams"), ("selection", "Whiteboard")])
|
||||
right = timer(timer_txt, low)
|
||||
if active == "Code":
|
||||
right += box("Gap", width="8px") + I("Button / Dark Small", "Run Button", Label="Run") + box("Gap", width="8px") + I("Button / Primary Small", "Submit Button", Label="Submit")
|
||||
return row(name, left + tabs + box("Spacer", flex_grow="1") + right + box("Pad", width="4px"), width=f"{TOOLBAR_W}px", height="52px", padding="0 8px 0 16px",
|
||||
background=D("panel"), border=f"1px solid {D('deep')}")
|
||||
|
||||
|
||||
reg("IDE toolbar", "IDE Toolbar / Code", toolbar("IDE Toolbar / Code", "Code"), "challenge toolbar, Code tab, Run + Submit")
|
||||
reg("IDE toolbar", "IDE Toolbar / Code Low Time", toolbar("IDE Toolbar / Code Low Time", "Code", True, "04:12"), "timer under 5 min turns red")
|
||||
reg("IDE toolbar", "IDE Toolbar / Diagrams", toolbar("IDE Toolbar / Diagrams", "Diagrams"), "Diagrams tab")
|
||||
reg("IDE toolbar", "IDE Toolbar / Whiteboard", toolbar("IDE Toolbar / Whiteboard", "Whiteboard"), "Whiteboard tab")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ File tree
|
||||
def tree_file(name, active, depth=1, ext_color="syntax-type"):
|
||||
c = fg("text-inverse") if active else D("text-dim")
|
||||
return row(f"File / {name}", ic("diamond", 10, fg(ext_color), "File Icon") + T("File Name", name, 14, c), gap="8px", padding=f"5px 8px 5px {depth * 14 + 10}px", border_radius="var(--radius-sm)",
|
||||
background=D("active") if active else "transparent")
|
||||
|
||||
|
||||
def file_tree(name, active):
|
||||
files = [("SearchBox.tsx", "syntax-type"), ("SearchBox.test.tsx", "syntax-type"), ("useDebounce.ts", "syntax-type"), ("styles.css", "ide-css")]
|
||||
tree = (row("Folder / src", ic("caret-down", 10, D("text-muted"), "Folder Icon") + T("Folder Name", "src", 14, D("text-muted"), 600), gap="6px", padding="5px 8px 5px 10px") +
|
||||
"".join(tree_file(n, n == active, 1, c) for n, c in files))
|
||||
return col(name, T("Explorer Title", "EXPLORER", 12, D("text-faint"), 600, letter_spacing="0.6px", padding="10px 14px") + col("Tree", tree, padding="8px", gap="0px"),
|
||||
width="210px", height=f"{BODY_H}px", background=D("panel"), border=f"1px solid {D('deep')}")
|
||||
|
||||
|
||||
reg("File tree", "File Tree / SearchBox Active", file_tree("File Tree / SearchBox Active", "SearchBox.tsx"), "Explorer, SearchBox.tsx selected")
|
||||
reg("File tree", "File Tree / Test Active", file_tree("File Tree / Test Active", "SearchBox.test.tsx"), "Explorer, test file selected")
|
||||
|
||||
|
||||
def editor_tab(name, active):
|
||||
c = fg("text-inverse") if active else D("text-muted")
|
||||
return col(f"Editor Tab / {name}", box("Ink Bar", height="2px", background=BRAND if active else "transparent") +
|
||||
row("Tab Body", ic("diamond", 10, fg("syntax-type"), "File Icon") + T("File Name", name, 14, c) + (ic("x", 12, D("text-faint"), "Close Icon") if active else ""), gap="8px", padding="7px 14px 8px"),
|
||||
background=fg("code-bg") if active else "transparent", border=f"1px solid {D('deep')}")
|
||||
|
||||
|
||||
for nm, (a, b) in {"SearchBox Active": ("SearchBox.tsx", "useDebounce.ts"), "Debounce Active": ("useDebounce.ts", "SearchBox.tsx")}.items():
|
||||
reg("Editor tabs", f"Editor Tabs / {nm}",
|
||||
row(f"Editor Tabs / {nm}", editor_tab(a, True) + editor_tab(b, False), width=f"{CENTER_W}px", height="37px", background=D("panel"), border=f"1px solid {D('deep')}", align_items="stretch"),
|
||||
"open file tabs")
|
||||
|
||||
# ------------------------------------------------------------------ Code editor (port of CodeEditor tokenizer)
|
||||
KW = re.compile(r"^(?:import|from|export|default|const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|await|async|new|class|interface|enum|extends|implements|public|private|protected|readonly|type|of|in|try|catch|finally|throw|typeof|instanceof|null|undefined|true|false|void|this|super|yield|as|static|get|set)\b")
|
||||
RULES = [("comment", re.compile(r"^//.*")), ("string", re.compile(r"^`(?:[^`\\]|\\.)*`?|^\"(?:[^\"\\]|\\.)*\"?|^'(?:[^'\\]|\\.)*'?")), ("keyword", KW),
|
||||
("type", re.compile(r"^[A-Z][A-Za-z0-9_]*")), ("number", re.compile(r"^\d+(?:\.\d+)?")), ("fn", re.compile(r"^[a-zA-Z_$][\w$]*(?=\s*\()")),
|
||||
("ident", re.compile(r"^[a-zA-Z_$][\w$]*")), ("ws", re.compile(r"^\s+"))]
|
||||
TOK_COLOR = {"comment": "syntax-comment", "string": "syntax-string", "keyword": "syntax-keyword", "type": "syntax-type", "number": "syntax-number", "fn": "syntax-fn"}
|
||||
|
||||
|
||||
def code_line_html(line):
|
||||
out, i = [], 0
|
||||
while i < len(line):
|
||||
s = line[i:]
|
||||
for cls, rx in RULES:
|
||||
m = rx.match(s)
|
||||
if m:
|
||||
t = m.group(0)
|
||||
out.append((cls, t))
|
||||
i += len(t)
|
||||
break
|
||||
else:
|
||||
out.append(("punct", line[i]))
|
||||
i += 1
|
||||
return "".join(f'<span style="color:var(--color-{TOK_COLOR[c]})">{_h.escape(t)}</span>' if c in TOK_COLOR else _h.escape(t) for c, t in out)
|
||||
|
||||
|
||||
SOLUTION = """import { useEffect, useRef, useState } from 'react';
|
||||
import { useDebounce } from './useDebounce';
|
||||
|
||||
type Result = { id: string; title: string };
|
||||
|
||||
export function SearchBox({ fetchResults }: Props) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState<Result[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const debounced = useDebounce(query, 300);
|
||||
const latest = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
// ignore responses that arrive out of order
|
||||
const ticket = ++latest.current;
|
||||
if (!debounced) return setResults([]);
|
||||
setLoading(true);
|
||||
fetchResults(debounced)
|
||||
.then((rows) => {
|
||||
if (ticket === latest.current) setResults(rows);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [debounced, fetchResults]);
|
||||
|
||||
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
|
||||
}"""
|
||||
DEBOUNCE = """import { useEffect, useState } from 'react';
|
||||
|
||||
export function useDebounce<T>(value: T, delayMs: number): T {
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => setDebounced(value), delayMs);
|
||||
return () => clearTimeout(id);
|
||||
}, [value, delayMs]);
|
||||
|
||||
return debounced;
|
||||
}"""
|
||||
|
||||
|
||||
def code_editor(name, src, h=609, w=CENTER_W, readonly=False):
|
||||
lines = src.split("\n")
|
||||
lh, fs = 20, 13
|
||||
gutter = col("Gutter", "".join(T(f"Ln {i}", str(i), fs, fg("syntax-gutter"), lh=lh, text_align="right", width="26px") for i in range(1, len(lines) + 1)),
|
||||
padding="12px 10px 12px 0", width="40px")
|
||||
body = []
|
||||
for i, ln in enumerate(lines, 1):
|
||||
ind = len(ln) - len(ln.lstrip(" "))
|
||||
body.append(box(f"Line {i}", code_line_html(ln.strip()) or " ", font_size=f"{fs}px", line_height=f"{lh}px", color=fg("code-fg"), font_weight=400,
|
||||
padding_left=f"{ind * 4}px", height=f"{lh}px"))
|
||||
return row(name, gutter + col("Code", "".join(body), padding="12px 16px", gap="0px"), width=f"{w}px", height=f"{h}px", background=D("deep"), overflow="hidden", align_items="flex-start")
|
||||
|
||||
|
||||
reg("Code editor", "Code Editor / SearchBox", code_editor("Code Editor / SearchBox", SOLUTION), "dark code area: gutter + highlighted lines (CodeEditor.tsx tokenizer)")
|
||||
reg("Code editor", "Code Editor / useDebounce", code_editor("Code Editor / useDebounce", DEBOUNCE), "second file")
|
||||
reg("Code editor", "Code Editor / Empty", code_editor("Code Editor / Empty", "// Write your solution here\n"), "seeded when the challenge ships no files")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Console
|
||||
def ctab(label, active):
|
||||
c = fg("text-inverse") if active else D("text-muted")
|
||||
return col(f"Console Tab / {label}", row("Tab Body", T("Label", label, 12, c), padding="8px 16px 7px") + box("Ink Bar", height="2px", background=BRAND if active else "transparent"))
|
||||
|
||||
|
||||
CONS = {"sys": "ide-text-muted", "pass": "ide-pass", "fail": "ide-fail", "info": "code-fg", "warn": "ide-warn"}
|
||||
|
||||
|
||||
def console(name, lines, running=False, empty=False):
|
||||
head = row("Console Tabs", ctab("Console", True) + ctab("Tests", False) + ctab("Terminal", False) + box("Spacer", flex_grow="1") + T("Clear", "Clear", 12, D("text-faint"), padding="0 14px"),
|
||||
background=D("panel"), border=f"1px solid {D('deep')}", height="34px")
|
||||
out = "".join(T(f"Output {i}", t, 13, fg(CONS[k]), lh=20) for i, (k, t) in enumerate(lines, 1))
|
||||
if empty:
|
||||
out = T("Output 1", "$ Console ready — run the tests to see output.", 13, D("text-faint"), lh=20)
|
||||
if running:
|
||||
out += row("Running", ic("cursor-text", 12, D("text-muted"), "Cursor Icon") + T("Running Label", "running…", 13, D("text-muted"), lh=20), gap="6px")
|
||||
return col(name, head + col("Output", out, padding="10px 16px", gap="0px", background=D("console"), height="162px", overflow="hidden"), width=f"{CENTER_W}px", height="196px")
|
||||
|
||||
|
||||
RUN_HEAD = [("sys", "$ npm test"), ("sys", "vitest run")]
|
||||
PASS = [("info", " ✓ debounces input by 300ms (312ms)"), ("info", " ✓ cancels stale requests (88ms)"), ("info", " ✓ shows loading state (24ms)"),
|
||||
("info", " ✓ renders empty state (19ms)"), ("pass", " ✓ handles out-of-order responses (41ms)"), ("info", " ✓ cleans up on unmount (12ms)")]
|
||||
reg("Console", "Console / Empty", console("Console / Empty", [], empty=True), "no output yet")
|
||||
reg("Console", "Console / Running", console("Console / Running", RUN_HEAD + PASS[:3], running=True), "tests streaming")
|
||||
reg("Console", "Console / Passed", console("Console / Passed", PASS[2:] + [("pass", " Test Files 1 passed (1)"), ("pass", " Tests 6 passed (6)"), ("pass", " Duration 2.05s")]), "all tests pass")
|
||||
reg("Console", "Console / Failed", console("Console / Failed", PASS[:2] + [("fail", " ✗ handles out-of-order responses (41ms)"), ("fail", " AssertionError: expected 'a' to equal 'ab'"),
|
||||
("info", " ✓ cleans up on unmount (12ms)"), ("fail", " Tests 1 failed | 5 passed (6)")]), "a failing test")
|
||||
|
||||
# ------------------------------------------------------------------ Problem rail
|
||||
PROMPT_P1 = "Build a SearchBox React component that queries a remote API as the user types."
|
||||
PROMPT_P2 = "Requests must be debounced by 300ms, stale in-flight requests must be ignored, and the UI must show loading and empty states. Make the provided tests pass."
|
||||
|
||||
|
||||
def problem_rail(name, open_=True):
|
||||
chips = row("Chips", T("Language Chip", "TypeScript", 12, fg("syntax-type"), lh=16, padding="2px 8px", background=D("blue-bg"), border_radius="var(--radius-sm)") +
|
||||
row("Time Chip", ic("clock", 12, D("text-muted"), "Clock Icon") + T("Time Label", "45m", 12, D("text-muted"), lh=16), gap="4px", padding="2px 8px", background=D("chip"), border_radius="var(--radius-sm)"), gap="8px")
|
||||
head = col("Rail Header", T("Title", "Debounced search box", 16, fg("text-inverse"), 700) + chips, gap="8px", padding="14px 16px 12px", border=f"1px solid {D('line')}")
|
||||
acc_h = row("Accordion Header", ic("caret-down" if open_ else "caret-right", 12, D("text"), "Caret Icon") + T("Accordion Title", "Problem brief", 14, "var(--color-ide-text)", 600, flex_grow="1"), gap="8px", padding="12px 16px")
|
||||
acc_b = col("Accordion Body", T("Brief", PROMPT_P1, 14, D("text-dim"), lh=22) + T("Brief 2", PROMPT_P2, 14, D("text-dim"), lh=22), gap="12px", padding="0 16px 16px") if open_ else ""
|
||||
return col(name, head + col("Accordion", acc_h + acc_b, border=f"1px solid {D('line')}"), width="316px", height=f"{BODY_H}px", background=D("rail"))
|
||||
|
||||
|
||||
reg("Problem rail", "Problem Rail / Brief", problem_rail("Problem Rail / Brief"), "right rail, brief open")
|
||||
reg("Problem rail", "Problem Rail / Collapsed", problem_rail("Problem Rail / Collapsed", False), "brief collapsed")
|
||||
|
||||
# ------------------------------------------------------------------ Diagram and whiteboard toolbars
|
||||
TEMPL = ["System design", "Sequence", "Class diagram", "State machine"]
|
||||
for i, act in enumerate(TEMPL):
|
||||
chips = "".join(T(f"Template / {t}", t, 12, fg("text-inverse") if t == act else D("text-muted"), lh=16, padding="4px 12px", border_radius="var(--radius-sm)",
|
||||
background=BRAND if t == act else D("line")) for t in TEMPL)
|
||||
reg("Diagram editor", f"Diagram Toolbar / {act}", row(f"Diagram Toolbar / {act}", T("Template Label", "Template:", 12, D("text-faint"), lh=16) + chips, gap="6px", width=f"{CENTER_W}px",
|
||||
padding="8px 14px", background=D("panel"), border=f"1px solid {D('deep')}"), "diagram template chips")
|
||||
|
||||
WB_COLORS = ["wb-white", "wb-blue", "ide-pass", "ide-fail", "ide-warn", "wb-purple", "wb-orange"]
|
||||
WB_TOOLS = [("Select", "cursor"), ("Rectangle", "rectangle"), ("Ellipse", "circle"), ("Arrow", "arrow-up-right"), ("Freehand", "pencil-simple"), ("Text", "text-t")]
|
||||
|
||||
|
||||
def wb_button(label, c, bg, icon=None):
|
||||
return row(f"Button / {label}", (ic(icon, 12, c, "Icon") if icon else "") + T("Label", label, 12, c, lh=16), gap="4px", padding="5px 10px", border_radius="6px", background=bg)
|
||||
|
||||
|
||||
def wb_toolbar(name, active, color_idx=0, selected=False):
|
||||
tools = "".join(row(f"Tool / {t}", ic(i, 18, fg("text-inverse") if t == active else fg("wb-text"), "Tool Icon"), justify_content="center", width="34px", height="34px", border_radius="7px",
|
||||
background=BRAND if t == active else fg("wb-button")) for t, i in WB_TOOLS)
|
||||
dots = "".join(box(f"Color / {c}", width="22px", height="22px", border_radius="99px", background=fg(c), border=f"2px solid {fg('text-inverse') if k == color_idx else 'transparent'}")
|
||||
for k, c in enumerate(WB_COLORS))
|
||||
right = (wb_button("Delete", fg("ide-fail"), D("fail-bg"), "x") if selected else "") + wb_button("Undo", fg("wb-text"), fg("wb-button"), "arrow-counter-clockwise") + wb_button("Clear", fg("wb-text"), fg("wb-button"))
|
||||
return row(name, tools + box("Divider", width="1px", height="26px", background=D("sep")) + dots + box("Spacer", flex_grow="1") + right, gap="6px", width=f"{CENTER_W + 316}px",
|
||||
padding="8px 12px", background=fg("wb-toolbar"), border=f"1px solid {fg('wb-border')}")
|
||||
|
||||
|
||||
reg("Whiteboard", "Whiteboard Toolbar / Rectangle", wb_toolbar("Whiteboard Toolbar / Rectangle", "Rectangle"), "tools, colors, undo, clear")
|
||||
reg("Whiteboard", "Whiteboard Toolbar / Select", wb_toolbar("Whiteboard Toolbar / Select", "Select", 0, True), "select tool with a selected shape (Delete shown)")
|
||||
reg("Whiteboard", "Whiteboard Toolbar / Arrow", wb_toolbar("Whiteboard Toolbar / Arrow", "Arrow", 1), "arrow tool, blue")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Theory phase
|
||||
def pips(states):
|
||||
out = ""
|
||||
for i, s in enumerate(states):
|
||||
out += box(f"Pip {i + 1}", width="28px" if s == "cur" else "8px", height="6px", border_radius="3px",
|
||||
background={"cur": fg("avatar-teal"), "done": fg("ide-pass"), "todo": fg("ide-line")}[s])
|
||||
return out
|
||||
|
||||
|
||||
def theory_toolbar(name, states, count, timer_txt):
|
||||
return row(name, row("Exit", ic("arrow-left", 14, D("text-faint"), "Exit Icon") + T("Exit Label", "Exit", 14, D("text-faint")), gap="6px") + box("Divider", width="1px", height="20px", background=D("sep")) +
|
||||
T("Phase Label", "RECRUITER SCREEN", 12, fg("avatar-teal"), 700, letter_spacing="0.8px") +
|
||||
row("Progress", pips(states) + T("Count", count, 12, D("text-faint"), padding_left="6px"), gap="4px", flex_grow="1") + timer(timer_txt),
|
||||
gap="12px", width=f"{TOOLBAR_W}px", height="52px", padding="0 20px", background=D("panel"), border=f"1px solid {D('deep')}")
|
||||
|
||||
|
||||
reg("Theory", "Theory Toolbar / Q2 of 5", theory_toolbar("Theory Toolbar / Q2 of 5", ["done", "cur", "todo", "todo", "todo"], "1/5", "27:45"), "question progress pips + timer")
|
||||
reg("Theory", "Theory Toolbar / Q5 of 5", theory_toolbar("Theory Toolbar / Q5 of 5", ["done", "done", "done", "done", "cur"], "4/5", "08:20"), "last question")
|
||||
|
||||
|
||||
def chip(name, label, c, bg):
|
||||
return row(name, T("Label", label, 12, fg(c), 600, lh=16), padding="3px 12px", background=bg, border_radius="99px")
|
||||
|
||||
|
||||
reg("Theory", "Theory Chip / Category", chip("Theory Chip / Category", "Behavioral", "avatar-teal", D("teal-bg")), "category pill")
|
||||
reg("Theory", "Theory Chip / Easy", chip("Theory Chip / Easy", "Easy", "ide-pass", D("pass-bg")), "difficulty")
|
||||
reg("Theory", "Theory Chip / Medium", chip("Theory Chip / Medium", "Medium", "ide-warn", D("warn-bg")), "difficulty")
|
||||
reg("Theory", "Theory Chip / Hard", chip("Theory Chip / Hard", "Hard", "ide-fail", D("fail-bg")), "difficulty")
|
||||
|
||||
|
||||
def tbtn(name, label, c, bg, border="transparent", w=None, weight=500, icon=None, after=False):
|
||||
ico = ic(icon, 14, c, "Icon") if icon else ""
|
||||
kids = (T("Label", label, 14, c, weight) + ico) if after else (ico + T("Label", label, 14, c, weight))
|
||||
return row(name, kids, gap="6px", padding="9px 22px", border_radius="6px", background=bg, border=f"1px solid {border}", justify_content="center")
|
||||
|
||||
|
||||
reg("Theory", "Theory Button / Prev", tbtn("Theory Button / Prev", "Prev", fg("ide-text-dim"), "transparent", D("line"), icon="arrow-left"), "outlined")
|
||||
reg("Theory", "Theory Button / Prev Disabled", tbtn("Theory Button / Prev Disabled", "Prev", fg("ide-line"), "transparent", D("line"), icon="arrow-left"), "first question")
|
||||
reg("Theory", "Theory Button / Next", tbtn("Theory Button / Next", "Next", fg("text-inverse"), fg("avatar-teal"), weight=600, icon="arrow-right", after=True), "teal")
|
||||
reg("Theory", "Theory Button / Submit Partial", tbtn("Theory Button / Submit Partial", "Submit (3/5 answered)", D("submit-fg"), D("submit-bg"), "#2a4060", weight=700), "not all answered")
|
||||
reg("Theory", "Theory Button / Submit All", tbtn("Theory Button / Submit All", "Submit answers", fg("text-inverse"), BRAND, BRAND, weight=700, icon="check", after=True), "all answered")
|
||||
|
||||
|
||||
def answer(name, txt, filled):
|
||||
return col(name, T("Answer", txt, 14, fg("code-fg") if filled else D("text-faint"), lh=24), width="704px", height="260px", padding="18px", background=D("input"), border_radius="var(--radius-md)",
|
||||
border=f"1px solid {D('pass-border') if filled else D('line')}")
|
||||
|
||||
|
||||
reg("Theory", "Theory Answer / Empty", answer("Theory Answer / Empty", "Type your answer here — explain your reasoning clearly.", False), "textarea placeholder")
|
||||
reg("Theory", "Theory Answer / Filled", answer("Theory Answer / Filled", "I would start by clarifying the team's goals and constraints, then walk the interviewer through a concrete example where I disagreed with a peer and how we reached a decision together.", True), "typed answer (>= 8 words: green border)")
|
||||
|
||||
# ------------------------------------------------------------------ Session home
|
||||
STEPS = ["Sign in", "System check", "Coding", "Submit"]
|
||||
|
||||
|
||||
def progress(name, cur):
|
||||
items = ""
|
||||
for i, s in enumerate(STEPS):
|
||||
done, now = i < cur, i == cur
|
||||
if done:
|
||||
inner = ic("check", 14, fg("text-inverse"), "Check Icon")
|
||||
else:
|
||||
inner = T("Step Number", str(i + 1), 12, fg("text-inverse") if now else fg("text-disabled"), 700, lh=16)
|
||||
dot = row("Step Dot", inner, justify_content="center", width="28px", height="28px", border_radius="99px",
|
||||
background=fg("success") if done else BRAND if now else fg("bg-elevated"), border="none" if (done or now) else f"2px solid {fg('border')}")
|
||||
items += col(f"Step / {s}", dot + T("Step Label", s, 12, fg("text-primary") if now else fg("text-secondary"), 600 if now else 400, lh=16, text_align="center"), align_items="center", gap="8px", flex_grow="1")
|
||||
line = row("Track", box("Line", height="2px", width="100%", background=fg("border-light")), position="absolute", left="76px", right="76px", top="13px")
|
||||
return col(name, T("Progress Title", "Your progress", 16, weight=600) + row("Steps", line + items, align_items="flex-start", position="relative", width="100%", gap="0px"), width="980px", gap="18px", padding="var(--spacing-lg)",
|
||||
background=fg("bg-elevated"), border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)", box_shadow="var(--shadow-sm)")
|
||||
|
||||
|
||||
reg("Session home", "Progress Tracker / System check", progress("Progress Tracker / System check", 1), "step 2 current")
|
||||
reg("Session home", "Progress Tracker / Coding", progress("Progress Tracker / Coding", 2), "step 3 current")
|
||||
|
||||
|
||||
def check_item(name, label, ok):
|
||||
dot = row("Dot", ic("check", 12, fg("text-inverse"), "Check Icon") if ok else "", justify_content="center", width="22px", height="22px", border_radius="99px", background=fg("success") if ok else fg("bg-layout"))
|
||||
return row(name, dot + T("Label", label, 14, fg("text-primary"), flex_grow="1") + T("Status", "Ready" if ok else "Stub", 12, fg("success") if ok else fg("text-disabled"), 600, lh=16),
|
||||
gap="12px", padding="12px 0", width="334px", border="1px solid transparent")
|
||||
|
||||
|
||||
reg("Session home", "Check Item / Ready", check_item("Check Item / Ready", "Browser compatible", True), "Ready")
|
||||
reg("Session home", "Check Item / Stub", check_item("Check Item / Stub", "Camera access", False), "not ready")
|
||||
|
||||
TIPS = {"Read carefully": ("target", "Take 5 minutes to fully understand the problem before you start typing."),
|
||||
"Think aloud": ("chat-circle-dots", "Verbalize your approach. Interviewers want to hear how you reason."),
|
||||
"Manage time": ("timer", "Plan to leave 10 minutes at the end for testing and cleanup.")}
|
||||
for t, (icn, d) in TIPS.items():
|
||||
reg("Session home", f"Tip Card / {t}", col(f"Tip Card / {t}", ic(icn, 22, fg("brand-primary"), "Tip Icon") + T("Tip Title", t, 14, weight=600) + T("Tip Desc", d, 12, fg("text-secondary"), lh=19),
|
||||
width="316px", gap="8px", padding="var(--spacing-md)", background=fg("bg-elevated"), border="1px solid var(--color-border-light)",
|
||||
border_radius="var(--radius-lg)", box_shadow="var(--shadow-sm)"), "tip")
|
||||
|
||||
# ------------------------------------------------------------------ System check (setup)
|
||||
SC = {"Ready": ("syscheck-ready", "check", fg("syscheck-ready"), fg("syscheck-ready-bg")), "Checking": ("brand-primary", "dots-three", BRAND, fg("focus-ring")),
|
||||
"Failed": ("syscheck-fail", "warning", fg("syscheck-fail"), "transparent"), "Pending": ("border-light", None, fg("text-disabled"), fg("border-light"))}
|
||||
BADGE = {"Ready": "Ready", "Checking": "Checking…", "Failed": "Action needed", "Pending": "—"}
|
||||
|
||||
|
||||
def sc_row(name, state, title, desc):
|
||||
ind_bg, icn, bfg, bbg = SC[state]
|
||||
ind = row("Indicator", ic(icn, 14, fg("text-inverse"), "State Icon") if icn else "", justify_content="center", width="26px", height="26px", border_radius="99px", background=fg(ind_bg))
|
||||
badge = row("Badge", T("Badge Label", BADGE[state], 12, bfg, 600, lh=16), padding="3px 10px", border_radius="5px", background=bbg)
|
||||
retry = row("Retry", T("Retry Label", "Retry", 12, fg("text-primary"), 500, lh=16), padding="3px 10px", border_radius="5px", background=fg("bg-elevated"), border=f"1px solid {fg('border')}") if state == "Failed" else ""
|
||||
return row(name, ind + col("Texts", T("Title", title, 14, weight=600, lh=18) + T("Desc", desc, 12, fg("text-secondary"), lh=16), flex_grow="1", gap="2px") + row("Status", badge + retry, gap="8px"),
|
||||
gap="14px", width="360px", padding="14px 0", border="1px solid transparent")
|
||||
|
||||
|
||||
for st in SC:
|
||||
reg("System check", f"Syscheck Row / {st}", sc_row(f"Syscheck Row / {st}", st, "Browser compatibility", "Chrome, Edge, or Firefox — up to date"), f"check row, {st}")
|
||||
|
||||
reg("System check", "Syscheck Step / Done", row("Syscheck Step / Done", row("Step Dot", ic("check", 12, fg("text-inverse"), "Check Icon"), justify_content="center", width="22px", height="22px", border_radius="99px", background=fg("syscheck-ready")) +
|
||||
T("Step Label", "Account setup", 14, fg("syscheck-ready"), 500), gap="8px"), "header step done")
|
||||
reg("System check", "Syscheck Step / Active", row("Syscheck Step / Active", row("Step Dot", T("Step Number", "2", 12, fg("text-inverse"), 700, lh=16), justify_content="center", width="22px", height="22px", border_radius="99px", background=BRAND) +
|
||||
T("Step Label", "System check", 14, BRAND, 500), gap="8px"), "header step active")
|
||||
|
||||
reg("Toast", "Message / Success", row("Message / Success", ic("check-circle", 16, fg("success"), "Success Icon") + T("Message", "Submission recorded", 14), gap="8px", padding="9px 12px",
|
||||
background=fg("bg-elevated"), border_radius="var(--radius-md)", box_shadow="var(--shadow-md)"), "antd message.success")
|
||||
|
||||
LIB_NAMES = [n for n, _ in SPECS]
|
||||
|
||||
|
||||
def build(P, x0, y0):
|
||||
ids, y = place_components(P, SPECS, x0, y0, 6000, GROUPS)
|
||||
return ids, y
|
||||
@@ -0,0 +1,17 @@
|
||||
from cand_common import *
|
||||
import collections
|
||||
fid,P,_=setup_cand()
|
||||
def walk(n,out):
|
||||
for c in P.c("get_children",nodeId=n)["children"]:
|
||||
if c["name"].startswith("Screen /"): out.append(c)
|
||||
elif c["name"].startswith(("Screens","Block","Section / Candidate")): walk(c["id"],out)
|
||||
out=[]
|
||||
for c in top_children(P):
|
||||
if c["name"] in ("Section / Candidate Access","Section / Candidate Session"): walk(c["id"],out)
|
||||
for c in out:
|
||||
r=P.lint(c["id"]); iss=r["issues"]
|
||||
print(c["name"],r.get("errors"),r.get("warnings"),r.get("byRule"))
|
||||
seen=set()
|
||||
for i in iss:
|
||||
k=(i.get("severity"),str(i.get("message"))[:70])
|
||||
if i.get("severity") in ("error","warning") and k not in seen: seen.add(k); print(" ",k)
|
||||
@@ -0,0 +1,12 @@
|
||||
import sys
|
||||
from cand_common import *
|
||||
fid,P,_=setup_cand()
|
||||
want=sys.argv[1:]
|
||||
def walk(n,out):
|
||||
for c in P.c("get_children",nodeId=n)["children"]:
|
||||
out.append(c);
|
||||
if c["name"].startswith(("Screens","Block","Section")) : walk(c["id"],out)
|
||||
out=[];walk("root",out)
|
||||
for c in out:
|
||||
if c["name"].startswith("Screen /") and (not want or any(w in c["name"] for w in want)):
|
||||
print(shot(P,c["id"],c["name"].replace(" ","_").replace("/","")))
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Build the Candidate page of the Interview project (idempotent): components, then the three sections.
|
||||
|
||||
python3 candidate.py [components] [access] [session] [challenge] (no args = everything)
|
||||
"""
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/Volumes/Storage/Projects/papercraft/examples/interview")
|
||||
from cand_common import *
|
||||
|
||||
LIB_Y = 1100 # imported Design System components sit at y 0..900; ours below
|
||||
|
||||
|
||||
def main(which):
|
||||
fid, P, DS = setup_cand()
|
||||
call("import_components", {"fileId": fid, "fromPageId": DS.id, "pageId": P.id})
|
||||
import cand_components as cc
|
||||
if "components" in which:
|
||||
names = set(cc.LIB_NAMES)
|
||||
print("deleted", delete_named(P, lambda n: n in names or n.startswith("Library / ")))
|
||||
cc.build(P, 0, LIB_Y)
|
||||
print("components", len(cc.LIB_NAMES))
|
||||
# sections go below the component library
|
||||
bottoms = [c.get("y", 0) + c["height"] for c in top_children(P) if not c["name"].startswith("Section / Candidate")]
|
||||
y0 = int(max(bottoms)) + 200
|
||||
for mod in ("access", "challenge"): # cand_access builds Access + Session
|
||||
if mod in which or (mod == "access" and "session" in which):
|
||||
m = __import__(f"cand_{mod}")
|
||||
print(mod, m.build(P, y0))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(set(sys.argv[1:]) or {"components", "access", "session", "challenge"})
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Entry point: builds the whole `Design System` page of the Interview project (idempotent).
|
||||
|
||||
python3 design_system.py
|
||||
|
||||
Foundations (ds_foundations.py) -> main components (ds_components.py, top-level frames, laid out in the
|
||||
library area right of the sections) -> Section / Components gallery (instances with captions).
|
||||
"""
|
||||
import ds_foundations
|
||||
import ds_components as C
|
||||
from ds_common import *
|
||||
|
||||
X_LIB = 1560
|
||||
|
||||
|
||||
def gallery(P):
|
||||
body = ""
|
||||
for g, names in C.GROUPS.items():
|
||||
cells = ""
|
||||
for n in names:
|
||||
cells += col(f"Cell / {n}", T("Caption", n, 12, "var(--color-text-secondary)", lh=16) + inst_html(n), gap="8px")
|
||||
body += col(f"Group / {g}", T("Group Title", g, 20, weight=600) + row("Cells", cells, flex_wrap="wrap", gap="var(--spacing-lg)", align_items="flex-start"),
|
||||
gap="var(--spacing-md)", padding="var(--spacing-lg)", background="var(--color-bg-layout)", border_radius="var(--radius-lg)", width="1376px")
|
||||
html = col("Section / Components", T("Section Title", "Components", 24, weight=700) +
|
||||
T("Section Intro", "Every item is an instance of a main component; main components live in the library area to the right. Use the exact names in <x-paper-instance component=...>.", 14, "var(--color-text-secondary)") +
|
||||
col("Groups", body, gap="var(--spacing-xl)"), width="1440px", gap="var(--spacing-xl)", padding="var(--spacing-xl)", background="transparent")
|
||||
return P.add("root", html)
|
||||
|
||||
|
||||
def stack(P, ids):
|
||||
y = 0
|
||||
for i in ids:
|
||||
P.c("update_styles", updates=[{"nodeIds": [i], "styles": {"left": 0, "top": y}}])
|
||||
h = next(c["height"] for c in P.c("get_children", nodeId="root")["children"] if c["id"] == i)
|
||||
y += h + 48
|
||||
return y
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fid, P = setup()
|
||||
clear(P)
|
||||
vals = call("get_variables", {"fileId": fid})["variables"]
|
||||
f_ids = ds_foundations.build(P, vals)
|
||||
ids, ylib = place_components(P, C.SPECS, X_LIB, 0, 2800, C.GROUPS)
|
||||
g_ids = gallery(P)
|
||||
stack(P, f_ids + g_ids)
|
||||
print("components:", len(ids))
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Shared helpers for the Design System page scripts (ds_foundations.py, ds_components.py, design_system.py)."""
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/Volumes/Storage/Projects/craft-skills/skills/papercraft/scripts")
|
||||
from pc import * # noqa: F401,F403,E402
|
||||
|
||||
PROJECT = "Interview"
|
||||
|
||||
# Extra color variables (no token in tokens.css: values read from the component code / AntD default theme).
|
||||
EXTRA_VARS = {
|
||||
# app sidebar (globals.css .app-sidebar*)
|
||||
"color-sidebar-bg": "#11161d",
|
||||
"color-sidebar-border": "#ffffff12",
|
||||
"color-sidebar-text": "#ffffffa6",
|
||||
"color-sidebar-text-muted": "#ffffff73",
|
||||
"color-sidebar-text-faint": "#ffffff59",
|
||||
"color-sidebar-hover": "#ffffff0f",
|
||||
"color-sidebar-badge": "#ffffff40",
|
||||
"color-sidebar-tag-bg": "#1677ff40",
|
||||
# Tag tones (Tag.tsx)
|
||||
"color-tag-blue-fg": "#0958d9", "color-tag-blue-bg": "#e6f4ff", "color-tag-blue-bd": "#91caff",
|
||||
"color-tag-green-fg": "#389e0d", "color-tag-green-bg": "#f6ffed", "color-tag-green-bd": "#b7eb8f",
|
||||
"color-tag-gold-fg": "#ad6800", "color-tag-gold-bg": "#fffbe6", "color-tag-gold-bd": "#ffe58f",
|
||||
"color-tag-red-fg": "#cf1322", "color-tag-red-bg": "#fff1f0", "color-tag-red-bd": "#ffccc7",
|
||||
"color-tag-purple-fg": "#531dab", "color-tag-purple-bg": "#f9f0ff", "color-tag-purple-bd": "#d3adf7",
|
||||
"color-tag-cyan-fg": "#08979c", "color-tag-cyan-bg": "#e6fffb", "color-tag-cyan-bd": "#87e8de",
|
||||
# misc component colors
|
||||
"color-button-dark": "#1f1f1f",
|
||||
"color-button-primary-hover": "#4096ff",
|
||||
"color-card-hover-border": "#bfdbff",
|
||||
"color-star": "#faad14",
|
||||
"color-score-warn": "#faad14",
|
||||
"color-score-poor": "#ff4d4f",
|
||||
"color-text-placeholder": "#00000040",
|
||||
"color-tooltip-bg": "#000000d9",
|
||||
"color-switch-off": "#00000040",
|
||||
"color-focus-ring": "#1677ff1a",
|
||||
# Avatar palette (Avatar.tsx PALETTE; blue/amber/green reuse brand/warning/success)
|
||||
"color-avatar-purple": "#722ed1", "color-avatar-teal": "#13a8a8", "color-avatar-pink": "#eb2f96",
|
||||
"color-avatar-orange": "#fa541c", "color-avatar-indigo": "#2f54eb",
|
||||
}
|
||||
|
||||
SHADOW_BTN = "0 1px 2px rgba(5,90,235,.25)"
|
||||
SHADOW_FOCUS = "0 0 0 2px var(--color-focus-ring)"
|
||||
|
||||
|
||||
def camel(k):
|
||||
k = k.replace("_", "-")
|
||||
p = k.split("-")
|
||||
return p[0] + "".join(x.title() for x in p[1:])
|
||||
|
||||
|
||||
def setup():
|
||||
"""Returns (file id, Design System Page) and defines the extra variables."""
|
||||
fid = next(i["id"] for i in call("list_resources")["items"] if i["name"] == PROJECT)
|
||||
P = pages(fid)["Design System"]
|
||||
call("set_variables", {"fileId": fid, "variables": EXTRA_VARS})
|
||||
return fid, P
|
||||
|
||||
|
||||
def clear(P):
|
||||
ids = [c["id"] for c in P.c("get_children", nodeId="root")["children"]]
|
||||
if ids:
|
||||
P.c("delete_nodes", nodeIds=ids)
|
||||
|
||||
|
||||
def lines(h): # shorthand for line-height in px
|
||||
return f"{h}px"
|
||||
|
||||
|
||||
def T(name, t, size=14, color="var(--color-text-primary)", weight=400, lh=None, **st):
|
||||
"""Text layer with px font size and a line-height (default ~1.5 rounded to 2px)."""
|
||||
lh = lh or {10.5: 14, 11: 16, 12: 16, 13: 18, 14: 22, 15: 20, 16: 24, 18: 26, 20: 28, 24: 32, 30: 36}.get(size, round(size * 1.4))
|
||||
return text(name, t, size=f"{size}px", color=color, weight=weight, line_height=f"{lh}px", **st)
|
||||
|
||||
|
||||
def ic(name, size=16, color="var(--color-text-secondary)", layer=None):
|
||||
return f'<x-paper-icon name="phosphor:{name}" size="{size}" color="{color}" layer-name="{layer or "Icon / " + name}"/>'
|
||||
|
||||
|
||||
def place_components(P, specs, x0, y0, max_w, groups):
|
||||
"""Create main components from [(name, html)] (html = ONE top-level element each) and lay them out in rows.
|
||||
|
||||
groups: ordered {group title: [component names]}; each group gets a title text and wrapped rows.
|
||||
"""
|
||||
ids = {}
|
||||
for i in range(0, len(specs), 12):
|
||||
chunk = specs[i : i + 12]
|
||||
r = P.c("write_html", targetNodeId="root", mode="insert-children", html="".join(h for _, h in chunk))
|
||||
for w in r.get("warnings", []):
|
||||
print(" warn:", w)
|
||||
if r.get("ignoredStyles"):
|
||||
print(" ignored:", sorted(set(r["ignoredStyles"]))[:6])
|
||||
for (name, _), nid in zip(chunk, r["createdNodeIds"]):
|
||||
P.c("create_component", nodeId=nid, name=name)
|
||||
ids[name] = nid
|
||||
sizes = {c["name"]: (c["width"], c["height"]) for c in P.c("get_components")["components"]}
|
||||
y = y0
|
||||
gap = 24
|
||||
for title, names in groups.items():
|
||||
t = P.add("root", T(f"Library / {title}", title, 20, weight=600, position="absolute", left=f"{x0}px", top=f"{y}px"))
|
||||
y += 40
|
||||
x, rowh = x0, 0
|
||||
for n in names:
|
||||
w, h = sizes[n]
|
||||
if x > x0 and x + w > x0 + max_w:
|
||||
x, y, rowh = x0, y + rowh + gap, 0
|
||||
P.c("update_styles", updates=[{"nodeIds": [ids[n]], "styles": {"left": x, "top": y}}])
|
||||
x += w + gap
|
||||
rowh = max(rowh, h)
|
||||
y += rowh + 48
|
||||
return ids, y
|
||||
@@ -0,0 +1,533 @@
|
||||
"""Main components of the Design System page (top-level frames) + the registry used by the gallery and the manifest."""
|
||||
from ds_common import *
|
||||
|
||||
SPECS = [] # [(name, html)]
|
||||
GROUPS = {} # group title -> [component names]
|
||||
NOTES = {} # component name -> short description (gallery caption)
|
||||
|
||||
|
||||
def reg(group, name, html, note=""):
|
||||
SPECS.append((name, html))
|
||||
GROUPS.setdefault(group, []).append(name)
|
||||
NOTES[name] = note
|
||||
|
||||
|
||||
def fg(c): # shorthand
|
||||
return f"var(--color-{c})"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Buttons
|
||||
BTN_SIZES = {"": (34, 16, 14), " Small": (28, 12, 12), " Large": (40, 20, 14)}
|
||||
BTN_VARIANTS = {
|
||||
"Primary": dict(background="var(--color-brand-primary)", border="1px solid var(--color-brand-primary)", box_shadow=SHADOW_BTN, color="var(--color-text-inverse)"),
|
||||
"Default": dict(background="var(--color-bg-elevated)", border="1px solid var(--color-border)", color="var(--color-text-primary)"),
|
||||
"Ghost": dict(background="transparent", border="1px solid transparent", color="var(--color-text-secondary)"),
|
||||
"Danger": dict(background="var(--color-bg-elevated)", border="1px solid var(--color-error)", color="var(--color-error)"),
|
||||
"Dark": dict(background="var(--color-button-dark)", border="1px solid var(--color-button-dark)", color="var(--color-text-inverse)"),
|
||||
}
|
||||
for v, st in BTN_VARIANTS.items():
|
||||
for suf, (h, px, fs) in BTN_SIZES.items():
|
||||
name = f"Button / {v}{suf}"
|
||||
col_ = st["color"]
|
||||
sty = {k: x for k, x in st.items() if k != "color"}
|
||||
reg("Buttons (packages/ui Button)", name,
|
||||
row(name, T("Label", "Button", fs, col_, 500), justify_content="center", gap="8px", height=f"{h}px",
|
||||
padding=f"0 {px}px", border_radius="var(--radius-md)", **sty),
|
||||
f"{h}px high, padding 0 {px}px, {fs}px label")
|
||||
reg("Buttons (packages/ui Button)", "Button / Notification Bell",
|
||||
row("Button / Notification Bell", ic("bell", 16, fg("text-primary"), "Bell") +
|
||||
box("Unread Dot", position="absolute", left="20px", top="6px", width="8px", height="8px", border_radius="99px", background=fg("error")),
|
||||
justify_content="center", position="relative", width="36px", height="36px", background=fg("bg-elevated"),
|
||||
border="1px solid var(--color-border)", border_radius="var(--radius-md)"),
|
||||
"AdminShell bell: 36x36, red unread dot")
|
||||
# AntD buttons (type primary etc.): 32px, radius 8, 14px
|
||||
ANT = {
|
||||
"Primary": dict(background=fg("brand-primary"), border="1px solid var(--color-brand-primary)", color=fg("text-inverse")),
|
||||
"Default": dict(background=fg("bg-elevated"), border="1px solid var(--color-border)", color=fg("text-primary")),
|
||||
"Danger": dict(background=fg("bg-elevated"), border="1px solid var(--color-error)", color=fg("error")),
|
||||
"Text": dict(background="transparent", border="1px solid transparent", color=fg("text-primary")),
|
||||
"Text Danger": dict(background="transparent", border="1px solid transparent", color=fg("error")),
|
||||
}
|
||||
for v, st in ANT.items():
|
||||
for suf, (h, px, fs) in {"": (32, 16, 14), " Small": (24, 8, 14), " Large": (40, 16, 16)}.items():
|
||||
name = f"Ant Button / {v}{suf}"
|
||||
sty = {k: x for k, x in st.items() if k != "color"}
|
||||
reg("Buttons (AntD Button)", name,
|
||||
row(name, T("Label", "Button", fs, st["color"]), justify_content="center", height=f"{h}px", padding=f"0 {px}px",
|
||||
border_radius="var(--radius-md)" if suf != " Small" else "var(--radius-sm)", **sty), f"AntD button {h}px")
|
||||
|
||||
# ---------------------------------------------------------------- Avatar
|
||||
AV = {"Avatar": (32, 12), "Avatar / 30": (30, 12), "Avatar / 34": (34, 13), "Avatar / 36": (36, 14), "Avatar / 40": (40, 16)}
|
||||
for name, (s, fs) in AV.items():
|
||||
reg("Avatar", name,
|
||||
row(name, T("Initials", "EV", fs, fg("text-inverse"), 600, letter_spacing="0.3px"), justify_content="center", width=f"{s}px", height=f"{s}px",
|
||||
border_radius="99px", background=fg("brand-primary")), f"{s}px circle, initials, colour by name hash")
|
||||
|
||||
# ---------------------------------------------------------------- Card / Stat
|
||||
reg("Card", "Card",
|
||||
col("Card", T("Card Title", "Card title", 16, weight=600) + T("Card Body", "Card content goes here.", 14, fg("text-secondary")),
|
||||
width="360px", gap="8px", padding="var(--spacing-lg)", background=fg("bg-elevated"), border="1px solid var(--color-border-light)",
|
||||
border_radius="var(--radius-lg)", box_shadow="var(--shadow-sm)"), "Card: 24px padding, radius-lg, shadow-sm")
|
||||
reg("Card", "Card / Hover",
|
||||
col("Card / Hover", T("Card Title", "Card title", 16, weight=600) + T("Card Body", "Card content goes here.", 14, fg("text-secondary")),
|
||||
width="360px", gap="8px", padding="var(--spacing-lg)", background=fg("bg-elevated"), border="1px solid var(--color-card-hover-border)",
|
||||
border_radius="var(--radius-lg)", box_shadow="var(--shadow-md)"), "hover: shadow-md, blue border")
|
||||
reg("Card", "Card / Compact",
|
||||
col("Card / Compact", T("Card Title", "Card title", 16, weight=600) + T("Card Body", "Card content goes here.", 14, fg("text-secondary")),
|
||||
width="360px", gap="4px", padding="var(--spacing-md)", background=fg("bg-elevated"), border="1px solid var(--color-border-light)",
|
||||
border_radius="var(--radius-lg)", box_shadow="var(--shadow-sm)"), "pad=md (16px)")
|
||||
|
||||
|
||||
def spark_svg(color, pts, name="Sparkline", w=72, h=30, gid=None):
|
||||
mn, mx = min(pts), max(pts)
|
||||
rng = (mx - mn) or 1
|
||||
n = len(pts) - 1
|
||||
p = [(i / n * w, h - (d - mn) / rng * (h - 4) - 2) for i, d in enumerate(pts)]
|
||||
line = " ".join(f"{x:.1f},{y:.1f}" for x, y in p)
|
||||
gid = gid or "g" + color[1:]
|
||||
return (f'<svg layer-name="{name}" width="{w}" height="{h}" viewBox="0 0 {w} {h}"><defs><linearGradient id="{gid}" x1="0" y1="0" x2="0" y2="1">'
|
||||
f'<stop offset="0%" stop-color="{color}" stop-opacity="0.22"/><stop offset="100%" stop-color="{color}" stop-opacity="0"/></linearGradient></defs>'
|
||||
f'<polygon points="0,{h} {line} {w},{h}" fill="url(#{gid})"/><polyline points="{line}" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>')
|
||||
|
||||
|
||||
UP = [3, 5, 4, 7, 6, 9, 8, 10]
|
||||
DOWN = [10, 9, 9, 7, 7, 5, 5, 4]
|
||||
SPARK_COLORS = {"Blue": "#1677ff", "Purple": "#722ed1", "Orange": "#d48806", "Green": "#52c41a", "Teal": "#13a8a8"}
|
||||
ACCENT_VAR = {"Blue": "brand-primary", "Purple": "avatar-purple", "Orange": "warning", "Green": "success", "Teal": "avatar-teal"}
|
||||
for cn, hexc in SPARK_COLORS.items():
|
||||
reg("Sparkline", f"Sparkline / {cn}", box(f"Sparkline / {cn}", spark_svg(hexc, UP, "Line", gid="s" + cn), display="flex"), "72x30 line + gradient area")
|
||||
reg("Sparkline", "Sparkline / Orange Down", box("Sparkline / Orange Down", spark_svg("#d48806", DOWN, "Line", gid="sod"), display="flex"), "downward trend")
|
||||
|
||||
|
||||
def stat(name, cn, down=False, hexc=None):
|
||||
hexc = hexc or SPARK_COLORS[cn]
|
||||
dc = fg("error") if down else fg("success")
|
||||
body = (T("Label", "ACTIVE CANDIDATES", 12, fg("text-secondary"), 500, letter_spacing="0.48px") +
|
||||
row("Value Row", T("Value", "12", 30, fg("text-primary"), 700) + spark_svg(hexc, DOWN if down else UP, "Sparkline", gid="st" + name.replace(" ", "").replace("/", "")),
|
||||
justify_content="space-between", gap="8px") +
|
||||
row("Delta Row", T("Delta Arrow", "▼" if down else "▲", 12, dc, 600) + T("Delta", "-1" if down else "+3", 12, dc, 600) + T("Trend", "this week", 12, fg("text-disabled")), gap="4px"))
|
||||
return row(name, box("Accent Bar", background=fg(ACCENT_VAR[cn]), width="4px", align_self="stretch") +
|
||||
col("Stat Body", body, gap="8px", padding="var(--spacing-lg)", flex_grow="1"),
|
||||
align_items="stretch", width="208px", background=fg("bg-elevated"), border="1px solid var(--color-border-light)",
|
||||
border_radius="var(--radius-lg)", box_shadow="var(--shadow-sm)", overflow="hidden")
|
||||
|
||||
|
||||
for cn in SPARK_COLORS:
|
||||
reg("Stat", f"Stat / {cn}", stat(f"Stat / {cn}", cn), "label, value 30/700, sparkline, delta line")
|
||||
reg("Stat", "Stat / Orange Down", stat("Stat / Orange Down", "Orange", True), "negative delta: red arrow")
|
||||
|
||||
# ---------------------------------------------------------------- Tag, status
|
||||
TONES = {"Blue": "blue", "Green": "green", "Gold": "gold", "Red": "red", "Purple": "purple", "Cyan": "cyan"}
|
||||
|
||||
|
||||
def tag(name, fgc, bgc, bdc, dot=False, label="Tag"):
|
||||
kids = (box("Dot", width="8px", height="8px", border_radius="99px", background=fgc) if dot else "") + T("Label", label, 12, fgc, 500, lh=16)
|
||||
return row(name, kids, gap="8px", height="22px", padding="0 8px", background=bgc, border=f"1px solid {bdc}", border_radius="var(--radius-sm)")
|
||||
|
||||
|
||||
for cn, t in TONES.items():
|
||||
f, b, d = (f"var(--color-tag-{t}-{x})" for x in ("fg", "bg", "bd"))
|
||||
reg("Tag", f"Tag / {cn}", tag(f"Tag / {cn}", f, b, d), "22px chip, 12px/500")
|
||||
reg("Tag", f"Tag Dot / {cn}", tag(f"Tag Dot / {cn}", f, b, d, True), "with status dot")
|
||||
reg("Tag", "Tag / Gray", tag("Tag / Gray", fg("text-secondary"), fg("bg-sider"), fg("border")), "neutral")
|
||||
reg("Tag", "Tag Dot / Gray", tag("Tag Dot / Gray", fg("text-secondary"), fg("bg-sider"), fg("border"), True), "neutral with dot")
|
||||
for nm, f, b, d in [("Default", fg("text-primary"), fg("bg-sider"), fg("border")),
|
||||
("Success", fg("success"), "var(--color-tag-green-bg)", "var(--color-tag-green-bd)"),
|
||||
("Warning", fg("warning"), "var(--color-tag-gold-bg)", "var(--color-tag-gold-bd)"),
|
||||
("Error", fg("error"), "var(--color-tag-red-bg)", "var(--color-tag-red-bd)"),
|
||||
("Processing", fg("brand-primary"), "var(--color-tag-blue-bg)", "var(--color-tag-blue-bd)")]:
|
||||
reg("Tag (AntD Tag status colors)", f"Ant Tag / {nm}", tag(f"Ant Tag / {nm}", f, b, d), "antd <Tag color=...>")
|
||||
for st, t in [("Queued", "blue"), ("Running", "gold"), ("Graded", "green"), ("Errored", "red")]:
|
||||
f, b, d = (f"var(--color-tag-{t}-{x})" for x in ("fg", "bg", "bd"))
|
||||
reg("Submission Status Badge", f"Submission Status Badge / {st}", tag(f"Submission Status Badge / {st}", f, b, d, label=st), "SubmissionStatusBadge")
|
||||
reg("Submission Status Badge", "Submission Status Badge / Unknown", tag("Submission Status Badge / Unknown", fg("text-primary"), fg("bg-sider"), fg("border"), label="Unknown"), "SubmissionStatusBadge")
|
||||
|
||||
# ---------------------------------------------------------------- Pill, Segmented, Search
|
||||
for act in (True, False):
|
||||
for cnt in (False, True):
|
||||
nm = f"Pill / {'Active' if act else 'Inactive'}{' With Count' if cnt else ''}"
|
||||
c = fg("brand-primary") if act else fg("text-secondary")
|
||||
kids = T("Label", "All", 12, c, 500, lh=16) + (T("Count", "12", 12, c, 700, lh=16, opacity="0.8") if cnt else "")
|
||||
reg("Pill (filter chip)", nm, row(nm, kids, gap="8px", height="30px", padding="0 12px", border_radius="99px",
|
||||
background=fg("bg-selected") if act else fg("bg-elevated"),
|
||||
border=f"1px solid {fg('brand-primary') if act else fg('border')}"), "filter chip, radius 99")
|
||||
|
||||
|
||||
def search(name, state):
|
||||
b = fg("brand-primary") if state == "focus" else fg("border")
|
||||
txt = T("Placeholder", "Search…" if state != "filled" else "backend", 14, fg("text-disabled") if state != "filled" else fg("text-primary"))
|
||||
return row(name, ic("magnifying-glass", 14, fg("text-disabled"), "Search Icon") + txt, gap="8px", width="260px", height="34px", padding="0 12px",
|
||||
background=fg("bg-elevated"), border=f"1px solid {b}", border_radius="var(--radius-md)",
|
||||
**({"box_shadow": SHADOW_FOCUS} if state == "focus" else {}))
|
||||
|
||||
|
||||
reg("Search Input", "Search Input", search("Search Input", "idle"), "260x34")
|
||||
reg("Search Input", "Search Input / Focused", search("Search Input / Focused", "focus"), "focus ring")
|
||||
reg("Search Input", "Search Input / Filled", search("Search Input / Filled", "filled"), "with value")
|
||||
|
||||
|
||||
def seg(n, a, small=False):
|
||||
nm = f"Segmented / {n} Options Active {a}"
|
||||
items = ""
|
||||
for k in range(1, n + 1):
|
||||
on = k == a
|
||||
items += row(f"Segment {k}", T(f"Option {k}", f"Option {k}", 12, fg("text-primary") if on else fg("text-secondary"), 600 if on else 500, lh=16),
|
||||
justify_content="center", height="28px", padding="0 16px", border_radius="var(--radius-sm)",
|
||||
background=fg("bg-elevated") if on else "transparent", **({"box_shadow": "var(--shadow-sm)"} if on else {}))
|
||||
return nm, row(nm, items, padding="4px", background=fg("border-light"), border_radius="var(--radius-md)")
|
||||
|
||||
|
||||
for n in (2, 3, 4):
|
||||
for a in range(1, n + 1):
|
||||
nm, h = seg(n, a)
|
||||
reg("Segmented (packages/ui)", nm, h, "track border-light, active = white + shadow-sm")
|
||||
|
||||
|
||||
def aseg(a):
|
||||
nm = f"Ant Segmented / 3 Options Active {a}"
|
||||
items = ""
|
||||
for k in range(1, 4):
|
||||
on = k == a
|
||||
items += row(f"Segment {k}", T(f"Option {k}", f"Option {k}", 14, fg("text-primary") if on else fg("text-secondary")), justify_content="center",
|
||||
height="28px", padding="0 12px", border_radius="var(--radius-md)", background=fg("bg-elevated") if on else "transparent",
|
||||
**({"box_shadow": "var(--shadow-sm)"} if on else {}))
|
||||
return nm, row(nm, items, padding="4px", background=fg("bg-layout"), border_radius="var(--radius-md)")
|
||||
|
||||
|
||||
for a in (1, 2, 3):
|
||||
nm, h = aseg(a)
|
||||
reg("Segmented (AntD)", nm, h, "antd Segmented (challenges page)")
|
||||
|
||||
# ---------------------------------------------------------------- Scores, stars, bar
|
||||
for nm, fill, w in [("High", "success", 128), ("Mid", "warning", 96), ("Low", "error", 48)]:
|
||||
reg("Score Bar / Dots / Stars", f"Score Bar / {nm}",
|
||||
box(f"Score Bar / {nm}", box("Fill", width=f"{w}px", height="6px", border_radius="99px", background=fg(fill)),
|
||||
display="flex", width="160px", height="6px", border_radius="99px", background=fg("border-light"), overflow="hidden"),
|
||||
"160x6 track; set Fill width in px")
|
||||
LABELS = {1: "Poor", 2: "Below bar", 3: "At bar", 4: "Above bar", 5: "Exceptional"}
|
||||
for v in range(0, 6):
|
||||
c = {5: fg("success"), 4: fg("success"), 3: fg("brand-primary"), 2: fg("score-warn"), 1: fg("score-poor"), 0: fg("score-poor")}[v]
|
||||
dots = ""
|
||||
for k in range(1, 6):
|
||||
on = k <= v
|
||||
dots += row(f"Dot {k}", T(f"Dot {k} Value", str(k) if on else "", 12, fg("text-inverse"), 600, lh=16), justify_content="center", width="26px", height="26px",
|
||||
border_radius="var(--radius-sm)", background=c if on else fg("bg-elevated"), border=f"1px solid {c if on else fg('border')}")
|
||||
nm = f"Score Dots / {v}"
|
||||
reg("Score Bar / Dots / Stars", nm, row(nm, row("Dots", dots, gap="4px") + T("Label", LABELS.get(v, ""), 12, fg("text-primary"), 600, lh=16, width="76px"), gap="16px"), "5 squares 26px + label")
|
||||
for v in range(0, 6):
|
||||
nm = f"Stars / {v}"
|
||||
reg("Score Bar / Dots / Stars", nm, row(nm, box("Stars", f'<span style="color:var(--color-star)">{"★" * v}</span><span style="color:var(--color-border)">{"☆" * (5 - v)}</span>',
|
||||
font_size="13px", letter_spacing="1px", line_height="16px")), "★ gold / ☆ border")
|
||||
|
||||
# ---------------------------------------------------------------- Donut / Spin / Empty / Loading
|
||||
import math
|
||||
|
||||
|
||||
def donut(segs, size=132, th=16):
|
||||
r = (size - th) / 2
|
||||
c = size / 2
|
||||
tot = sum(v for v, _ in segs)
|
||||
a0 = -math.pi / 2
|
||||
paths = ""
|
||||
for v, col_ in segs:
|
||||
a1 = a0 + v / tot * 2 * math.pi
|
||||
large = 1 if (a1 - a0) > math.pi else 0
|
||||
paths += f'<path d="M{c + r * math.cos(a0):.2f} {c + r * math.sin(a0):.2f} A{r} {r} 0 {large} 1 {c + r * math.cos(a1):.2f} {c + r * math.sin(a1):.2f}" fill="none" stroke="{col_}" stroke-width="{th}"/>'
|
||||
a0 = a1
|
||||
ring = f'<svg layer-name="Ring" width="{size}" height="{size}" viewBox="0 0 {size} {size}"><circle cx="{c}" cy="{c}" r="{r}" fill="none" stroke="#f0f0f0" stroke-width="{th}"/>{paths}</svg>'
|
||||
center = col("Center", T("Total", "42", 26, fg("text-primary"), 700, lh=32) + T("Caption", "sessions", 12, fg("text-secondary"), lh=16), align_items="center",
|
||||
justify_content="center", position="absolute", left="0px", top="0px", width=f"{size}px", height=f"{size}px")
|
||||
return ring + center
|
||||
|
||||
|
||||
reg("Donut / Spin / Empty / Loading", "Donut", box("Donut", donut([(18, "#52c41a"), (12, "#1677ff"), (8, "#faad14"), (4, "#ff4d4f")]), position="relative", display="flex", width="132px", height="132px"),
|
||||
"132px ring, 16px stroke; segments = stroke colors")
|
||||
|
||||
|
||||
def spin(name, s):
|
||||
r = s * 0.2
|
||||
o = s * 0.35
|
||||
c = s / 2
|
||||
pts = [(c, c - o, 1), (c + o, c, .3), (c, c + o, .3), (c - o, c, .3)]
|
||||
cs = "".join(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r:.1f}" fill="#1677ff" fill-opacity="{op}"/>' for x, y, op in pts)
|
||||
return box(name, f'<svg layer-name="Dots" width="{s}" height="{s}" viewBox="0 0 {s} {s}">{cs}</svg>', display="flex")
|
||||
|
||||
|
||||
reg("Donut / Spin / Empty / Loading", "Spin / Small", spin("Spin / Small", 16), "antd Spin small")
|
||||
reg("Donut / Spin / Empty / Loading", "Spin", spin("Spin", 20), "antd Spin default")
|
||||
reg("Donut / Spin / Empty / Loading", "Spin / Large", spin("Spin / Large", 32), "antd Spin large")
|
||||
reg("Donut / Spin / Empty / Loading", "Loading Block",
|
||||
row("Loading Block", spin("Spin Icon", 32) + T("Tip", "Loading…", 14, fg("text-secondary")), justify_content="center", gap="8px", padding="var(--spacing-xl)", width="360px"),
|
||||
"LoadingBlock: Spin + tip")
|
||||
|
||||
EMPTY_SVG = ('<svg layer-name="Illustration" width="64" height="41" viewBox="0 0 64 41"><ellipse cx="32" cy="33" rx="32" ry="7" fill="#f5f5f5"/>'
|
||||
'<path d="M55 12.8 44.9 1.3C44.4.5 43.7 0 42.9 0H21.1c-.7 0-1.5.5-1.9 1.3L9 12.8V22h46z" fill="#fafafa" stroke="#d9d9d9"/>'
|
||||
'<path d="M41.6 16c0-1.7 1-3 2.3-3H55v18c0 2-1.5 3.5-3.4 3.5H12.4C10.5 34.5 9 33 9 31V13h11.1c1.3 0 2.3 1.3 2.3 3 0 1.6 1 3 2.3 3h14.6c1.3 0 2.3-1.4 2.3-3z" fill="#ffffff" stroke="#d9d9d9"/></svg>')
|
||||
reg("Donut / Spin / Empty / Loading", "Empty / Simple",
|
||||
col("Empty / Simple", EMPTY_SVG + T("Description", "No data", 14, fg("text-disabled")), align_items="center", gap="8px", padding="var(--spacing-xl)", width="360px"), "antd Empty (simple image)")
|
||||
reg("Donut / Spin / Empty / Loading", "Empty State",
|
||||
col("Empty State", EMPTY_SVG + col("Texts", T("Title", "No sessions yet", 14, fg("text-primary"), 500, text_align="center") +
|
||||
T("Description", "Create a session to get started.", 14, fg("text-secondary"), text_align="center"), align_items="center"),
|
||||
align_items="center", gap="8px", padding="var(--spacing-xl)", width="360px"), "EmptyState: title + description")
|
||||
|
||||
# ---------------------------------------------------------------- Page headers
|
||||
HEAD_W = "1192px"
|
||||
|
||||
|
||||
def title_block(t, s):
|
||||
return col("Titles", T("Title", t, 24, fg("text-primary"), 600, lh=32) + T("Subtitle", s, 14, fg("text-secondary")), gap="0px")
|
||||
|
||||
|
||||
reg("Page headers", "Page Header",
|
||||
row("Page Header", title_block("Page title", "Page subtitle") , justify_content="space-between", width=HEAD_W, padding="var(--page-padding)",
|
||||
background=fg("bg-elevated"), border="1px solid var(--color-border-light)"), "PageHeader w/o actions (1192 = 1440 - sidebar)")
|
||||
reg("Page headers", "Admin Shell Header",
|
||||
row("Admin Shell Header", title_block("Dashboard", "Welcome to your dashboard") +
|
||||
row("Actions", "<x-paper-instance component=\"Button / Notification Bell\"/>", gap="12px"),
|
||||
justify_content="space-between", width=HEAD_W, padding="var(--page-padding)", background=fg("bg-elevated"), border="1px solid var(--color-border-light)"),
|
||||
"AdminShell header: title, subtitle, bell (instance of Button / Notification Bell)")
|
||||
reg("Page headers", "Candidate Shell Header",
|
||||
col("Candidate Shell Header", row("Header Card", title_block("Smoke Candidate", "Session af17f53a") +
|
||||
row("Exit Session", ic("sign-out", 16, fg("text-primary"), "Exit Icon") + T("Exit Label", "Exit session", 14, fg("text-primary")), justify_content="center", gap="8px", height="34px",
|
||||
padding="0 16px", background=fg("bg-elevated"), border="1px solid var(--color-border)", border_radius="var(--radius-md)"),
|
||||
justify_content="space-between", width="1144px", padding="var(--page-padding)", background=fg("bg-elevated")),
|
||||
width=HEAD_W, padding="16px 24px", background=fg("bg-layout")), "CandidateShell header (white card inset on layout bg)")
|
||||
reg("Page headers", "Section Title",
|
||||
row("Section Title", col("Texts", T("Title", "Hiring pipeline", 16, fg("text-primary"), 600, lh=22) + T("Subtitle", "Across all open roles", 12, fg("text-secondary"), lh=16)),
|
||||
justify_content="space-between", width="360px"), "SectionTitle: 16/600 + 12 sub")
|
||||
reg("Page headers", "Section Title / With Action",
|
||||
row("Section Title / With Action", col("Texts", T("Title", "Upcoming interviews", 16, fg("text-primary"), 600, lh=22) + T("Subtitle", "Next 7 days", 12, fg("text-secondary"), lh=16)) +
|
||||
T("Action", "Library →", 14, fg("text-secondary")), justify_content="space-between", width="360px"), "with action slot")
|
||||
|
||||
# ---------------------------------------------------------------- Data table
|
||||
TW = "960px"
|
||||
|
||||
|
||||
def cells(prefix, texts, **st):
|
||||
return "".join(T(f"{prefix} {i}", t, st.pop("size", 14) if False else st.get("size", 14), st.get("color", fg("text-primary")), st.get("weight", 400),
|
||||
flex_grow="1", **({"letter_spacing": "0.36px"} if st.get("ls") else {})) for i, t in enumerate(texts, 1))
|
||||
|
||||
|
||||
def tcells(prefix, texts, size, color, weight, ls=None):
|
||||
return "".join(T(f"{prefix} {i}", t, size, color, weight, flex="1", **({"letter_spacing": ls} if ls else {})) for i, t in enumerate(texts, 1))
|
||||
|
||||
|
||||
reg("Data Table (packages/ui DataTable)", "Data Table / Header Row",
|
||||
row("Data Table / Header Row", tcells("Col", ["TITLE", "TYPE", "STATUS", "SCORE", "UPDATED"], 12, fg("text-secondary"), 600, "0.36px"), width=TW, padding="12px 16px", gap="16px",
|
||||
background=fg("bg-sider"), border="1px solid var(--color-border-light)"), "uppercase 12/600; hide unused Col n with display:none")
|
||||
for nm, bgc in [("Row", "bg-elevated"), ("Row Hover", "bg-selected")]:
|
||||
reg("Data Table (packages/ui DataTable)", f"Data Table / {nm}",
|
||||
row(f"Data Table / {nm}", tcells("Cell", ["Two Sum", "Coding", "Active", "4.2", "Jun 12"], 14, fg("text-primary"), 400), width=TW, padding="12px 16px", gap="16px",
|
||||
background=fg(bgc), border="1px solid var(--color-border-light)"), "13px vertical padding in code (comfortable)")
|
||||
|
||||
|
||||
def page_btn(n, on=False, name=None):
|
||||
return row(name or f"Page {n}", T("Page Label", n, 14, fg("brand-primary") if on else fg("text-primary")), justify_content="center", width="32px", height="32px",
|
||||
border_radius="var(--radius-md)", background=fg("bg-elevated"), border=f"1px solid {fg('brand-primary') if on else fg('border')}" if on else "1px solid transparent")
|
||||
|
||||
|
||||
PAGER = (row("Prev", ic("caret-left", 14, fg("text-secondary"), "Prev Icon"), justify_content="center", width="32px", height="32px") + page_btn("1", True) + page_btn("2") + page_btn("3") +
|
||||
page_btn("…", name="Page More") + page_btn("9") + row("Next", ic("caret-right", 14, fg("text-secondary"), "Next Icon"), justify_content="center", width="32px", height="32px"))
|
||||
reg("Data Table (packages/ui DataTable)", "Pagination", row("Pagination", PAGER, gap="8px"), "antd Pagination: 32px items, active = blue border")
|
||||
reg("Data Table (packages/ui DataTable)", "Data Table / Footer",
|
||||
row("Data Table / Footer", T("Summary", "Showing 1-5 of 42", 12, fg("text-secondary"), lh=16) + row("Pager", PAGER, gap="8px"), justify_content="space-between", width=TW,
|
||||
padding="12px 16px", background=fg("bg-elevated")), "summary + pagination")
|
||||
reg("Data Table (packages/ui DataTable)", "Data Table / Empty",
|
||||
row("Data Table / Empty", T("Message", "No rows match your filters.", 14, fg("text-disabled"), text_align="center"), justify_content="center", width=TW, padding="var(--spacing-xxl)",
|
||||
background=fg("bg-elevated")), "emptyMessage, 48px padding")
|
||||
reg("Ant Table", "Ant Table / Header Row",
|
||||
row("Ant Table / Header Row", tcells("Col", ["Title", "Code", "Status", "Created", "Actions"], 14, fg("text-primary"), 600), width=TW, padding="16px", gap="16px",
|
||||
background=fg("bg-sider"), border="1px solid var(--color-border-light)"), "antd Table th (#fafafa, 14/600)")
|
||||
reg("Ant Table", "Ant Table / Row",
|
||||
row("Ant Table / Row", tcells("Cell", ["Backend Engineer", "A1B2C3", "Scheduled", "Jun 12", "View"], 14, fg("text-primary"), 400), width=TW, padding="16px", gap="16px",
|
||||
background=fg("bg-elevated"), border="1px solid var(--color-border-light)"), "antd Table td, row ~55px")
|
||||
|
||||
# ---------------------------------------------------------------- Form controls
|
||||
CTRL = dict(background=fg("bg-elevated"), border_radius="var(--radius-md)")
|
||||
|
||||
|
||||
def inp(name, ph="", val=None, large=False, state="", w=280, prefix=None, suffix=None, disabled=False):
|
||||
h, fs, pad = (40, 16, 12) if large else (32, 14, 12)
|
||||
b = {"focus": fg("brand-primary"), "error": fg("error")}.get(state, fg("border"))
|
||||
kids = (T("Prefix", prefix, fs, fg("text-secondary")) if prefix else "")
|
||||
kids += T("Value", val if val is not None else ph, fs, fg("text-disabled") if disabled else (fg("text-primary") if val is not None else fg("text-placeholder")), flex_grow="1")
|
||||
kids += suffix or ""
|
||||
st = dict(CTRL)
|
||||
if disabled:
|
||||
st["background"] = fg("bg-layout")
|
||||
if state == "focus":
|
||||
st["box_shadow"] = "0 0 0 2px #1677ff33"
|
||||
if state == "error":
|
||||
st["box_shadow"] = "0 0 0 2px #cf132233"
|
||||
return row(name, kids, gap="4px", width=f"{w}px", height=f"{h}px", padding=f"0 {pad}px", border=f"1px solid {b}", **st)
|
||||
|
||||
|
||||
reg("Input (AntD)", "Input", inp("Input", "Placeholder"), "32px, radius 8, border #d9d9d9")
|
||||
reg("Input (AntD)", "Input / Filled", inp("Input / Filled", val="Netcracker"), "with value")
|
||||
reg("Input (AntD)", "Input / Focus", inp("Input / Focus", val="Netcracker", state="focus"), "focus ring")
|
||||
reg("Input (AntD)", "Input / Error", inp("Input / Error", val="invalid", state="error"), "error state")
|
||||
reg("Input (AntD)", "Input / Disabled", inp("Input / Disabled", val="Disabled", disabled=True), "disabled")
|
||||
reg("Input (AntD)", "Input / Large", inp("Input / Large", "Placeholder", large=True), "size=large: 40px / 16px (forms)")
|
||||
reg("Input (AntD)", "Input / Large Filled", inp("Input / Large Filled", val="Netcracker", large=True), "size=large with value")
|
||||
reg("Input (AntD)", "Input / Large Focus", inp("Input / Large Focus", val="Netcracker", large=True, state="focus"), "size=large focus")
|
||||
reg("Input (AntD)", "Input / Large Error", inp("Input / Large Error", val="invalid", large=True, state="error"), "size=large error")
|
||||
reg("Input (AntD)", "Input / With Prefix", inp("Input / With Prefix", val="netcracker", large=True, prefix="hireloop.io/"), "Workspace URL field")
|
||||
reg("Input (AntD)", "Input / Password", inp("Input / Password", "Password", large=True, suffix=ic("eye", 16, fg("text-disabled"), "Eye Icon")), "Input.Password large")
|
||||
reg("Input (AntD)", "Input / Search", inp("Input / Search", "Search templates…", suffix=ic("magnifying-glass", 14, fg("text-disabled"), "Search Icon")), "Input.Search")
|
||||
reg("Input (AntD)", "Input Number",
|
||||
row("Input Number", T("Value", "60", 14, fg("text-primary"), flex_grow="1") +
|
||||
col("Steppers", row("Up", ic("caret-up", 10, fg("text-disabled"), "Up Icon"), justify_content="center", height="15px") + row("Down", ic("caret-down", 10, fg("text-disabled"), "Down Icon"), justify_content="center", height="15px"), width="22px"),
|
||||
gap="4px", width="120px", height="32px", padding="0 0 0 12px", border="1px solid var(--color-border)", **CTRL), "InputNumber (steppers on the right)")
|
||||
reg("Input (AntD)", "Date Picker", inp("Date Picker", "Select date", w=200, suffix=ic("calendar-blank", 14, fg("text-disabled"), "Calendar Icon")), "DatePicker")
|
||||
reg("Input (AntD)", "Textarea",
|
||||
row("Textarea", T("Value", "Brief description of your team or hiring process", 14, fg("text-placeholder"), flex_grow="1"), align_items="flex-start", width="280px", height="76px",
|
||||
padding="4px 12px", border="1px solid var(--color-border)", **CTRL), "Input.TextArea, 3 rows")
|
||||
reg("Input (AntD)", "Textarea / Filled",
|
||||
row("Textarea / Filled", T("Value", "Write your answer here.", 14, fg("text-primary"), flex_grow="1"), align_items="flex-start", width="280px", height="76px",
|
||||
padding="4px 12px", border="1px solid var(--color-border)", **CTRL), "filled")
|
||||
for large in (False, True):
|
||||
for filled in (False, True):
|
||||
nm = f"Select{' / Large' if large else ''}{' Filled' if filled and large else (' / Filled' if filled else '')}"
|
||||
reg("Select (AntD)", nm, inp(nm, "Select…", val="UTC" if filled else None, large=large, suffix=ic("caret-down", 12, fg("text-disabled"), "Arrow Icon")), "Select closed")
|
||||
reg("Select (AntD)", "Select / Dropdown",
|
||||
col("Select / Dropdown", "".join(row(f"Option {i}", T(f"Option {i} Label", t, 14, fg("text-primary"), 600 if i == 2 else 400), height="32px", padding="0 12px", border_radius="var(--radius-sm)",
|
||||
**({"background": fg("bg-selected")} if i == 2 else {})) for i, t in enumerate(["UTC", "UTC+3 (Moscow)", "UTC+1 (CET)"], 1)),
|
||||
width="280px", padding="4px", background=fg("bg-elevated"), border_radius="var(--radius-md)", box_shadow="var(--shadow-md)"), "open options panel; selected = bg-selected + 600")
|
||||
|
||||
for chk in (False, True):
|
||||
nm = f"Checkbox / {'Checked' if chk else 'Unchecked'}"
|
||||
bx = row("Box", ic("check", 12, fg("text-inverse"), "Check Icon") if chk else "", justify_content="center", width="16px", height="16px", border_radius="var(--radius-sm)",
|
||||
background=fg("brand-primary") if chk else fg("bg-elevated"), border=f"1px solid {fg('brand-primary') if chk else fg('border')}")
|
||||
reg("Checkbox / Radio / Switch", nm, row(nm, bx + T("Label", "Keep me signed in", 14, fg("text-primary")), gap="8px"), "16px box")
|
||||
for sel in (False, True):
|
||||
nm = f"Radio / {'Selected' if sel else 'Unselected'}"
|
||||
bx = row("Circle", box("Dot", width="8px", height="8px", border_radius="99px", background=fg("brand-primary")) if sel else "", justify_content="center", width="16px", height="16px",
|
||||
border_radius="99px", background=fg("bg-elevated"), border=f"1px solid {fg('brand-primary') if sel else fg('border')}")
|
||||
reg("Checkbox / Radio / Switch", nm, row(nm, bx + T("Label", "Option label", 14, fg("text-primary")), gap="8px"), "radio")
|
||||
for on in (False, True):
|
||||
nm = f"Switch / {'On' if on else 'Off'}"
|
||||
reg("Checkbox / Radio / Switch", nm, row(nm, box("Knob", width="18px", height="18px", border_radius="99px", background=fg("bg-elevated")), width="44px", height="22px", padding="0 4px",
|
||||
border_radius="99px", background=fg("brand-primary") if on else fg("switch-off"), justify_content="flex-end" if on else "flex-start"), "44x22")
|
||||
|
||||
|
||||
def field(name, label, req=False, err=None, help_=None, val=None, large=True):
|
||||
lab = row("Label Row", (T("Required Mark", "*", 14, fg("error")) if req else "") + T("Label", label, 14, fg("text-primary")), gap="4px", height="32px")
|
||||
ctrl = inp("Input", "Placeholder", val=val, large=large, state="error" if err else "", w=480)
|
||||
tail = T("Help", err or help_, 14, fg("error") if err else fg("text-secondary")) if (err or help_) else ""
|
||||
return col(name, lab + ctrl + tail, gap="0px", width="480px")
|
||||
|
||||
|
||||
reg("Form Field", "Form Field", field("Form Field", "Workspace name", val="Netcracker"), "label + large input (AntD Form.Item vertical)")
|
||||
reg("Form Field", "Form Field / Required", field("Form Field / Required", "Email", req=True), "* required mark")
|
||||
reg("Form Field", "Form Field / Error", field("Form Field / Error", "Email", req=True, err="Please enter a valid email", val="x@"), "error + message")
|
||||
reg("Form Field", "Form Field / With Help", field("Form Field / With Help", "Workspace URL", help_="Used in invite links."), "help text")
|
||||
|
||||
# ---------------------------------------------------------------- Modal, Drawer, Tabs, Alert, Tooltip, Progress, Skeleton
|
||||
reg("Overlays", "Modal",
|
||||
col("Modal", row("Header", T("Title", "Modal title", 16, fg("text-primary"), 600) + ic("x", 14, fg("text-secondary"), "Close Icon"), justify_content="space-between") +
|
||||
T("Body", "Modal body content.", 14, fg("text-primary"), height="96px") +
|
||||
row("Footer", row("Cancel Button", T("Cancel Label", "Cancel", 14, fg("text-primary")), justify_content="center", height="32px", padding="0 16px", border="1px solid var(--color-border)", border_radius="var(--radius-md)", background=fg("bg-elevated")) +
|
||||
row("OK Button", T("OK Label", "OK", 14, fg("text-inverse")), justify_content="center", height="32px", padding="0 16px", background=fg("brand-primary"), border_radius="var(--radius-md)"),
|
||||
justify_content="flex-end", gap="8px"),
|
||||
width="520px", gap="12px", padding="20px 24px", background=fg("bg-elevated"), border_radius="var(--radius-lg)", box_shadow="var(--shadow-lg)"), "antd Modal; put on a bg-overlay frame")
|
||||
reg("Overlays", "Drawer",
|
||||
col("Drawer", row("Header", T("Title", "Notes (markdown)", 16, fg("text-primary"), 600) + ic("x", 14, fg("text-secondary"), "Close Icon"), justify_content="space-between", padding="16px 24px", border="1px solid var(--color-border-light)") +
|
||||
col("Body Wrap", T("Body", "Drawer body", 14, fg("text-primary")), padding="var(--spacing-lg)"), width="420px", height="900px", background=fg("bg-elevated"), box_shadow="var(--shadow-lg)"),
|
||||
"antd Drawer (right, full height)")
|
||||
reg("Overlays", "Tooltip",
|
||||
col("Tooltip", row("Bubble", T("Text", "Resolve all conflicts first", 14, fg("text-inverse")), padding="6px 12px", background=fg("tooltip-bg"), border_radius="var(--radius-md)") +
|
||||
'<svg layer-name="Arrow" width="16" height="6" viewBox="0 0 16 6"><polygon points="0,0 16,0 8,6" fill="#000000" fill-opacity="0.85"/></svg>', align_items="center"), "antd Tooltip")
|
||||
for t, (bg_, bd_, icn, icc) in {"Info": ("tag-blue-bg", "tag-blue-bd", "info", "brand-primary"), "Success": ("tag-green-bg", "tag-green-bd", "check-circle", "success"),
|
||||
"Warning": ("tag-gold-bg", "tag-gold-bd", "warning-circle", "score-warn"), "Error": ("tag-red-bg", "tag-red-bd", "x-circle", "score-poor")}.items():
|
||||
for icon_on in (False, True):
|
||||
nm = f"Alert / {t}{' With Icon' if icon_on else ''}"
|
||||
reg("Alert", nm, row(nm, (ic(icn, 16, fg(icc), "Alert Icon") if icon_on else "") + T("Message", f"{t} message goes here.", 14, fg("text-primary"), flex_grow="1"),
|
||||
gap="8px", width="400px", padding="8px 12px", background=fg(bg_), border=f"1px solid {fg(bd_)}", border_radius="var(--radius-md)"), "antd Alert")
|
||||
for i, nm_ in enumerate(["Tab"], 1):
|
||||
pass
|
||||
for on in (True, False):
|
||||
for large in (False, True):
|
||||
nm = f"Tabs / Tab {'Active' if on else 'Inactive'}{' Large' if large else ''}"
|
||||
reg("Tabs", nm, col(nm, row("Tab Pad", T("Label", "Workspace", 16 if large else 14, fg("brand-primary") if on else fg("text-primary")), padding="12px 0") +
|
||||
box("Ink Bar", align_self="stretch", height="2px", background=fg("brand-primary") if on else "transparent"), align_items="stretch"), "antd Tabs item (ink bar when active)")
|
||||
|
||||
|
||||
def tabbar(labels, a, large):
|
||||
items = ""
|
||||
for k, l in enumerate(labels, 1):
|
||||
on = k == a
|
||||
items += col(f"Tab {k}", row(f"Tab {k} Pad", T(f"Tab {k} Label", l, 16 if large else 14, fg("brand-primary") if on else fg("text-primary")), padding="12px 0") +
|
||||
box(f"Tab {k} Ink", align_self="stretch", height="2px", background=fg("brand-primary") if on else "transparent"))
|
||||
return items
|
||||
|
||||
|
||||
for a in (1, 2, 3):
|
||||
nm = f"Tabs / Bar 3 Active {a}"
|
||||
reg("Tabs", nm, col(nm, row("Tabs", tabbar(["Overview", "Details", "History"], a, False), gap="32px") + box("Divider", align_self="stretch", height="1px", background=fg("border-light")), width="480px", gap="0px"), "antd Tabs bar, 14px")
|
||||
L5 = ["Workspace", "Team", "Scoring", "Notifications", "Integrations"]
|
||||
for a in range(1, 6):
|
||||
nm = f"Tabs / Bar 5 Large Active {a}"
|
||||
reg("Tabs", nm, col(nm, row("Tabs", tabbar(L5, a, True), gap="32px") + box("Divider", align_self="stretch", height="1px", background=fg("border-light")), width="720px", gap="0px"), "configuration page tabs (size=large)")
|
||||
|
||||
reg("Misc AntD", "Progress / Circle",
|
||||
box("Progress / Circle", '<svg layer-name="Ring" width="64" height="64" viewBox="0 0 64 64"><circle cx="32" cy="32" r="29" fill="none" stroke="#f0f0f0" stroke-width="6"/>'
|
||||
'<path d="M32 3 A29 29 0 1 1 8.6 49" fill="none" stroke="#52c41a" stroke-width="6" stroke-linecap="round"/></svg>' +
|
||||
row("Center", T("Value", "85", 16, fg("text-primary")), justify_content="center", position="absolute", left="0px", top="0px", width="64px", height="64px"),
|
||||
position="relative", display="flex", width="64px", height="64px"), "Progress type=circle size=64")
|
||||
reg("Misc AntD", "Skeleton",
|
||||
col("Skeleton", box("Title Bar", width="152px", height="16px", border_radius="var(--radius-sm)", background=fg("border-light")) +
|
||||
"".join(box(f"Line {i}", width="400px" if i < 4 else "244px", height="16px", border_radius="var(--radius-sm)", background=fg("border-light")) for i in range(1, 5)),
|
||||
gap="16px", width="400px"), "antd Skeleton active (static)")
|
||||
|
||||
# ---------------------------------------------------------------- App shell
|
||||
NAV = {"Workspace": [("Dashboard", "squares-four"), ("Challenges", "list-bullets"), ("Question bank", "database"), ("Sessions", "clock"), ("Schedule", "calendar")],
|
||||
"Manage": [("Configuration", "gear"), ("Rubric", "table")]}
|
||||
|
||||
|
||||
def nav_item(label, icon, active=False):
|
||||
c = fg("text-inverse") if active else fg("sidebar-text")
|
||||
return row(f"Nav / {label}", ic(icon, 18, c, f"Nav Icon / {label}") + T(f"Nav Label / {label}", label, 14, c, 600 if active else 500, flex_grow="1"),
|
||||
gap="12px", height="36px", padding="0 12px", border_radius="var(--radius-md)", background=fg("brand-primary") if active else "transparent")
|
||||
|
||||
|
||||
def brand(mark_html, sub):
|
||||
return row("Brand", row("Brand Mark", mark_html, justify_content="center", width="30px", height="30px", border_radius="var(--radius-md)", background=fg("brand-primary")) +
|
||||
col("Brand Text", T("Brand Name", "Hireloop", 16, fg("text-inverse"), 700, lh=18) + T("Brand Sub", sub, 12, fg("sidebar-text-muted"), lh=16), gap="0px"),
|
||||
gap="8px", padding="16px 16px 12px")
|
||||
|
||||
|
||||
def user_footer(name, email_or_role, tag=None, admin=True):
|
||||
ini = "".join(p[0] for p in name.split()[:2]).upper()
|
||||
meta = row("User Name Row", T("User Name", name, 13, fg("text-inverse"), 600, lh=18) + (row("User Role Tag", T("User Role Tag Label", tag, 12, fg("brand-secondary"), 700, lh=16), padding="0 8px", border_radius="var(--radius-sm)", background=fg("sidebar-tag-bg")) if tag else ""), gap="8px") + \
|
||||
T("User Email", email_or_role, 12, fg("sidebar-text-muted"), lh=16)
|
||||
right = (row("Footer Actions", row("Settings Button", ic("gear", 16, fg("sidebar-text-muted"), "Settings Icon"), justify_content="center", width="28px", height="28px") +
|
||||
row("Sign Out Button", ic("sign-out", 16, fg("sidebar-text-muted"), "Sign Out Icon"), justify_content="center", width="28px", height="28px")) if admin else ic("caret-down", 14, fg("sidebar-text-muted"), "Chevron Icon"))
|
||||
return row("User Footer", row("User Avatar", T("User Initials", ini, 13, fg("text-inverse"), 600, lh=16), justify_content="center", width="34px", height="34px", border_radius="99px", background=fg("brand-primary")) +
|
||||
col("User Meta", meta, flex_grow="1", gap="0px") + right, gap="8px", padding="var(--spacing-md)", border="1px solid var(--color-sidebar-border)")
|
||||
|
||||
|
||||
def admin_sidebar(name, active=None):
|
||||
groups = ""
|
||||
for g, items in NAV.items():
|
||||
groups += col(f"Group / {g}", T(f"Group Label / {g}", g.upper(), 12, fg("sidebar-text-faint"), 600, lh=16, letter_spacing="0.72px", padding="0 12px 8px") +
|
||||
"".join(nav_item(l, i, l == active) for l, i in items), gap="4px", padding="0 0 16px")
|
||||
return col(name, brand(T("Brand Mark Letter", "H", 14, fg("text-inverse"), 700, lh=18), "Admin workspace") + col("Nav", groups, gap="0px", padding="4px 16px 16px", flex_grow="1") + user_footer("Admin", "admin@test.local", "ADMIN"),
|
||||
width="248px", height="900px", gap="0px", background=fg("sidebar-bg"), border="1px solid var(--color-sidebar-border)")
|
||||
|
||||
|
||||
reg("App shell", "Admin Sidebar", admin_sidebar("Admin Sidebar"), "248x900 dark nav; no active item")
|
||||
for g, items in NAV.items():
|
||||
for l, _ in items:
|
||||
reg("App shell", f"Admin Sidebar / {l} Active", admin_sidebar(f"Admin Sidebar / {l} Active", l), f"{l} highlighted")
|
||||
|
||||
|
||||
def cand_sidebar(name, active=None):
|
||||
its = [("Home", "house"), ("My interview", "code")]
|
||||
return col(name, brand(ic("command", 16, fg("text-inverse"), "Brand Mark Icon"), "Candidate") +
|
||||
col("Nav", "".join(nav_item(l, i, l == active) for l, i in its), gap="4px", padding="8px 16px", flex_grow="1") + user_footer("Smoke Candidate", "Candidate", admin=False),
|
||||
width="248px", height="900px", gap="0px", background=fg("sidebar-bg"), border="1px solid var(--color-sidebar-border)")
|
||||
|
||||
|
||||
reg("App shell", "Candidate Sidebar", cand_sidebar("Candidate Sidebar"), "248x900; no active item")
|
||||
reg("App shell", "Candidate Sidebar / Home Active", cand_sidebar("Candidate Sidebar / Home Active", "Home"), "Home highlighted")
|
||||
reg("App shell", "Candidate Sidebar / My interview Active", cand_sidebar("Candidate Sidebar / My interview Active", "My interview"), "My interview highlighted")
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Foundation documentation sections: colors, typography, spacing, radius & shadow, page layout."""
|
||||
from ds_common import *
|
||||
|
||||
W = 1440
|
||||
|
||||
|
||||
def section(name, title, body, intro=None):
|
||||
kids = T("Section Title", title, 24, weight=700) + (T("Section Intro", intro, 14, fg2) if intro else "") + body
|
||||
return col(name, kids, width=f"{W}px", gap="var(--spacing-xl)", padding="var(--spacing-xl)", background="transparent")
|
||||
|
||||
|
||||
fg2 = "var(--color-text-secondary)"
|
||||
|
||||
|
||||
def card(name, title, body, w=None, **st):
|
||||
return col(name, T("Card Title", title, 16, weight=600) + body, gap="var(--spacing-md)", padding="var(--spacing-lg)", background="var(--color-bg-elevated)",
|
||||
border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)", **({"width": w} if w else {}), **st)
|
||||
|
||||
|
||||
ALPHA = {"color-bg-overlay": "rgba(0,0,0,.45)", "color-text-primary": "rgba(0,0,0,.88)", "color-text-secondary": "rgba(0,0,0,.65)", "color-text-disabled": "rgba(0,0,0,.45)",
|
||||
"color-sidebar-border": "rgba(255,255,255,.07)", "color-sidebar-text": "rgba(255,255,255,.65)", "color-sidebar-text-muted": "rgba(255,255,255,.45)",
|
||||
"color-sidebar-text-faint": "rgba(255,255,255,.35)", "color-sidebar-hover": "rgba(255,255,255,.06)", "color-sidebar-badge": "rgba(255,255,255,.25)",
|
||||
"color-sidebar-tag-bg": "rgba(22,119,255,.25)", "color-text-placeholder": "rgba(0,0,0,.25)", "color-tooltip-bg": "rgba(0,0,0,.85)", "color-switch-off": "rgba(0,0,0,.25)",
|
||||
"color-focus-ring": "rgba(22,119,255,.10)"}
|
||||
|
||||
COLOR_GROUPS = {
|
||||
"Brand": ["color-brand-primary", "color-brand-secondary", "color-text-link", "color-info", "color-button-primary-hover"],
|
||||
"Background": ["color-bg-layout", "color-bg-elevated", "color-bg-sider", "color-bg-selected", "color-bg-overlay"],
|
||||
"Border": ["color-border", "color-border-light", "color-border-strong", "color-card-hover-border"],
|
||||
"Text": ["color-text-primary", "color-text-secondary", "color-text-disabled", "color-text-inverse", "color-text-placeholder"],
|
||||
"Status": ["color-success", "color-warning", "color-error", "color-star", "color-score-warn", "color-score-poor"],
|
||||
"Diff & code": ["color-diff-added", "color-diff-removed", "color-diff-neutral", "color-code-bg", "color-code-fg"],
|
||||
"Sidebar (dark shell)": ["color-sidebar-bg", "color-sidebar-border", "color-sidebar-text", "color-sidebar-text-muted", "color-sidebar-text-faint", "color-sidebar-hover", "color-sidebar-badge", "color-sidebar-tag-bg"],
|
||||
"Tag tones": [f"color-tag-{t}-{x}" for t in ("blue", "green", "gold", "red", "purple", "cyan") for x in ("fg", "bg", "bd")],
|
||||
"Avatar palette": ["color-brand-primary", "color-avatar-purple", "color-avatar-teal", "color-warning", "color-avatar-pink", "color-success", "color-avatar-orange", "color-avatar-indigo"],
|
||||
"Controls": ["color-button-dark", "color-tooltip-bg", "color-switch-off", "color-focus-ring"],
|
||||
}
|
||||
|
||||
|
||||
def swatch(var, val):
|
||||
shown = ALPHA.get(var) or val
|
||||
return row(f"Swatch / {var}", row("Chip Bg", box("Chip", width="40px", height="40px", border_radius="var(--radius-md)", background=f"var(--{var})", border="1px solid var(--color-border-light)"), background="var(--color-sidebar-bg)" if "sidebar" in var and var != "color-sidebar-bg" else "transparent", border_radius="var(--radius-md)") +
|
||||
col("Names", T("Token Name", var.replace("color-", ""), 14, weight=600, lh=18) + T("Token Var", f"var(--{var})", 12, fg2, lh=16), gap="0px", flex_grow="1") +
|
||||
T("Token Value", shown, 12, fg2, lh=16), gap="12px")
|
||||
|
||||
|
||||
def colors(vals):
|
||||
cards = ""
|
||||
for g, names in COLOR_GROUPS.items():
|
||||
cards += card(f"Colors / {g}", g, col("Swatches", "".join(swatch(n, vals[n]) for n in names), gap="12px"), w="440px")
|
||||
body = row("Color Groups", cards, flex_wrap="wrap", gap="var(--spacing-lg)", align_items="flex-start")
|
||||
return section("Section / Colors", "Colors", body, "Every color is a PaperCraft variable named exactly like the CSS custom property in packages/ui/src/tokens.css, minus the leading --. Use var(--color-brand-primary) etc.")
|
||||
|
||||
|
||||
FONT = [("font-size-xs", 12, "Labels, table headers (uppercase 600), Tag, Pill, captions", 16),
|
||||
("font-size-sm", 14, "Body, buttons, inputs, nav items, table cells", 22),
|
||||
("font-size-md", 16, "SectionTitle, card titles, large inputs, brand name", 24),
|
||||
("font-size-lg", 18, "Large headings (token only)", 26),
|
||||
("font-size-xl", 20, "Large headings (token only)", 28),
|
||||
("font-size-xxl", 24, "Page titles (PageHeader h3, 600)", 32)]
|
||||
OTHER = [(30, "Stat value (700)"), (26, "Donut total (700)"), (15, "Sidebar brand name (700)"), (13, "Sidebar user name (600), Avatar initials"), (12, "Sidebar group label, role tag (clamped from 10-11)")]
|
||||
|
||||
|
||||
def typography():
|
||||
rows = ""
|
||||
for tok, px, use, lh in FONT:
|
||||
rows += row(f"Type / {tok}", T("Type Token", tok, 12, fg2, width="140px", lh=16) + T("Type Size", f"{px}px / {lh}px", 12, fg2, width="100px", lh=16) +
|
||||
T("Type Sample", "The quick brown fox jumps over the lazy dog", px, lh=lh, width="620px") + T("Type Usage", use, 12, fg2, flex_grow="1", lh=16), gap="var(--spacing-md)")
|
||||
other = "".join(row(f"Type / {px}px", T("Type Token", "(literal)", 12, fg2, width="140px", lh=16) + T("Type Size", f"{px}px", 12, fg2, width="100px", lh=16) +
|
||||
T("Type Sample", "Hireloop 12 42", px, weight=700 if px in (30, 26, 15) else 400, width="620px") + T("Type Usage", use, 12, fg2, flex_grow="1", lh=16), gap="var(--spacing-md)") for px, use in OTHER)
|
||||
wts = row("Weights", "".join(col(f"Weight / {w}", T("Weight Sample", "Aa Hireloop", 20, weight=w) + T("Weight Note", note, 12, fg2, lh=16), gap="4px") for w, note in
|
||||
[(400, "400 regular: body"), (500, "500 medium: buttons, tags (renders as 400: Inter regular/bold only)"), (600, "600 semibold: titles, headers (renders bold)"), (700, "700 bold: stat values, brand")]), gap="var(--spacing-xl)", align_items="flex-start")
|
||||
ant = "".join(row(f"AntD / {n}", T("Type Token", n, 12, fg2, width="140px", lh=16) + T("Type Size", f"{px}px / {lh}px", 12, fg2, width="100px", lh=16) +
|
||||
T("Type Sample", "Page heading", px, weight=600, lh=lh, width="620px") + T("Type Usage", u, 12, fg2, flex_grow="1", lh=16), gap="var(--spacing-md)")
|
||||
for n, px, lh, u in [("Typography.Title 1", 38, 46, "unused"), ("Typography.Title 3", 24, 32, "PageHeader title, login heading"), ("Typography.Title 4", 20, 28, "card headings"), ("Typography.Title 5", 16, 24, "card section titles (600)")])
|
||||
body = card("Type scale", "Type scale (font-size tokens)", col("Rows", rows, gap="var(--spacing-md)")) + card("Type literals", "Sizes used literally in the code", col("Rows", other, gap="var(--spacing-md)")) + \
|
||||
card("Type weights", "Weights", wts) + card("Type AntD", "AntD Typography.Title (all text is Inter)", col("Rows", ant, gap="var(--spacing-md)"))
|
||||
return section("Section / Typography", "Typography", col("Type Cards", body, gap="var(--spacing-lg)"), "Single family: Inter (the source uses the system UI stack). Line-height ~1.5 (1.35 for headings).")
|
||||
|
||||
|
||||
def spacing():
|
||||
bars = ""
|
||||
for n, px in [("xs", 4), ("sm", 8), ("md", 16), ("lg", 24), ("xl", 32), ("xxl", 48)]:
|
||||
bars += row(f"Space / {n}", T("Space Token", f"spacing-{n}", 14, weight=600, width="120px") + T("Space Value", f"{px}px", 12, fg2, width="48px", lh=16) +
|
||||
box("Space Bar", width=f"var(--spacing-{n})", height="16px", background="var(--color-brand-primary)", border_radius="var(--radius-sm)"), gap="var(--spacing-md)")
|
||||
for n, px in [("page-padding", 24), ("page-padding-lg", 48)]:
|
||||
bars += row(f"Space / {n}", T("Space Token", n, 14, weight=600, width="120px") + T("Space Value", f"{px}px", 12, fg2, width="48px", lh=16) +
|
||||
box("Space Bar", width=f"var(--{n})", height="16px", background="var(--color-brand-secondary)", border_radius="var(--radius-sm)"), gap="var(--spacing-md)")
|
||||
return section("Section / Spacing", "Spacing", card("Spacing scale", "Spacing scale (4px grid)", col("Bars", bars, gap="var(--spacing-md)"), w="720px"),
|
||||
"Tokens spacing-xs..xxl and page-padding. Off-scale values in the code: 2/5/6/7/9/10/12/14/18/20/22.")
|
||||
|
||||
|
||||
def radius_shadow():
|
||||
rad = "".join(col(f"Radius / {n}", box("Radius Sample", width="96px", height="64px", background="var(--color-bg-selected)", border="1px solid var(--color-brand-primary)", border_radius=f"var(--radius-{n})") +
|
||||
T("Radius Token", f"radius-{n}", 14, weight=600, lh=18) + T("Radius Value", f"{px}px: {u}", 12, fg2, lh=16), gap="8px")
|
||||
for n, px, u in [("sm", 4, "Tag, Segmented item, ScoreDots"), ("md", 8, "Button, inputs, nav items, AntD controls"), ("lg", 12, "Card, DataTable, Modal")])
|
||||
rad += col("Radius / pill", box("Radius Sample", width="96px", height="64px", background="var(--color-bg-selected)", border="1px solid var(--color-brand-primary)", border_radius="99px") +
|
||||
T("Radius Token", "99px (literal)", 14, weight=600, lh=18) + T("Radius Value", "Pill, ScoreBar, Switch, Avatar 50%", 12, fg2, lh=16), gap="8px")
|
||||
shd = "".join(col(f"Shadow / {n}", box("Shadow Sample", width="160px", height="96px", background="var(--color-bg-elevated)", border="1px solid var(--color-border-light)", border_radius="var(--radius-lg)", box_shadow=f"var(--shadow-{n})") +
|
||||
T("Shadow Token", f"shadow-{n}", 14, weight=600, lh=18) + T("Shadow Value", v, 12, fg2, lh=16), gap="8px")
|
||||
for n, v in [("sm", "0 1px 2px rgba(0,0,0,.06): Card, Segmented active"), ("md", "0 4px 12px rgba(0,0,0,.08): Card hover, dropdowns"), ("lg", "0 8px 24px rgba(0,0,0,.12): Modal, Drawer")])
|
||||
stage = row("Shadow Stage", shd, gap="var(--spacing-lg)", padding="var(--spacing-lg)", background="var(--color-bg-layout)", border_radius="var(--radius-lg)", align_items="flex-start")
|
||||
return section("Section / Radius & Shadow", "Radius & Shadow", row("Radius Shadow Cards", card("Radius", "Radius", row("Radius Row", rad, gap="var(--spacing-lg)", align_items="flex-start")) + card("Shadow", "Shadows (on cards, over bg-layout)", stage), gap="var(--spacing-lg)", align_items="flex-start"))
|
||||
|
||||
|
||||
def page_layout():
|
||||
widths = [("xs", 480, "invite/verify card"), ("sm", 420, ""), ("md / narrow", 720, "narrow content"), ("lg / default", 900, ".app-content default"), ("xl / wide", 1100, ".app-content--wide")]
|
||||
bars = "".join(row(f"Width / {n}", T("Width Token", f"page-max-width-{n}", 14, weight=600, width="220px") + T("Width Value", f"{px}px", 12, fg2, width="56px", lh=16) +
|
||||
box("Width Bar", width=f"{px//2}px", height="16px", background="var(--color-brand-secondary)", border_radius="var(--radius-sm)") + T("Width Use", u, 12, fg2, lh=16), gap="var(--spacing-md)") for n, px, u in widths)
|
||||
shell = col("Shell facts", "".join(T(f"Fact {i}", t, 14) for i, t in enumerate([
|
||||
"App shell: sidebar 248px (flex 0 0 248) + main column; desktop frames are 1440x900.", "Page header: 24px padding, ~102px tall (title 24/600 + subtitle 14).",
|
||||
"Content: padding 24 (page-padding), max-width 900 default / 1100 wide; centered.", "Candidate: white header card inset (padding 16 24) on bg-layout.", "Mobile (<=640px): sidebar hidden, padding 16."], 1)), gap="8px")
|
||||
z = "".join(row(f"Z / {n}", T("Z Token", f"z-{n}", 14, weight=600, width="140px") + T("Z Value", str(v), 14, fg2, width="64px") + T("Z Use", u, 12, fg2, lh=16), gap="var(--spacing-md)")
|
||||
for n, v, u in [("base", 1, "sticky table header"), ("dropdown", 1000, "Select / menus"), ("modal", 1050, "Modal, Drawer"), ("toast", 1100, "message / notifications")])
|
||||
return section("Section / Page Layout", "Page Layout", row("Layout Cards", card("Max widths", "Page max-widths (bars drawn at 50%)", col("Bars", bars, gap="var(--spacing-md)")) + col("Layout Right", card("Shell", "App shell", shell) + card("Z-index", "z-index scale", col("Zs", z, gap="var(--spacing-sm)")), gap="var(--spacing-lg)"),
|
||||
gap="var(--spacing-lg)", align_items="flex-start"))
|
||||
|
||||
|
||||
def build(P, vals):
|
||||
ids = []
|
||||
for html in (colors(vals), typography(), spacing(), radius_shadow(), page_layout()):
|
||||
ids += P.add("root", html)
|
||||
return ids
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Writes DESIGN_SYSTEM.md from the component registry (names, captions, text layers) + hand-written guidance."""
|
||||
import re, sys
|
||||
sys.path.insert(0, ".")
|
||||
import ds_components as C
|
||||
from ds_foundations import COLOR_GROUPS
|
||||
from ds_common import EXTRA_VARS
|
||||
|
||||
LEAF = re.compile(r'layer-name="([^"]+)"[^>]*>([^<]+)</div>')
|
||||
out = ["# Interview design system (PaperCraft page `Design System`)", "",
|
||||
"Generated by `design_system.py` + `gen_manifest.py`. Main components are top-level frames on the `Design System` page (library area, right of the doc sections, x >= 1560). Documentation sections: `Section / Colors`, `Section / Typography`, `Section / Spacing`, `Section / Radius & Shadow`, `Section / Page Layout`, `Section / Components` (captioned instances of everything).", "",
|
||||
"## How to use", "",
|
||||
"1. On your page: `papercraft-cli call import_components '{\"fromPageId\":\"<Design System page id>\",\"pageId\":\"<your page id>\"}'` (skips names already there; if you delete a copy and re-import, its instances lose overrides).",
|
||||
"2. Place: `<x-paper-instance component=\"Button / Primary\" overrides='{\"Label\":\"Save\"}'/>` or `inst_html(\"Button / Primary\", Label=\"Save\")` from pc.py.",
|
||||
"3. Overrides JSON: key = exact layer name inside the component (first match), value = string (text) or object (CSS camelCase styles, e.g. `{\"Label\":{\"color\":\"var(--color-error)\"}}`). Hide a part: `{\"Col 5\":{\"display\":\"none\"}}`.",
|
||||
"4. The instance root accepts a `style` attribute: `style=\"width:720px\"`, `style=\"background-color:var(--color-avatar-purple)\"`, `style=\"opacity:.5\"` (disabled).",
|
||||
"5. Icons inside components cannot be recolored or swapped by overrides (vector layers). That is why the sidebars exist as one variant per active item. For anything else, compose in raw HTML with the recipes below.",
|
||||
"6. Components hug their content (fixed width where noted). To stretch, set `style=\"width:Npx\"` on the instance. Uppercase labels (table headers, Stat label, sidebar group labels) are typed uppercase: text-transform is not supported.",
|
||||
"7. Instances are page-local; no variants/properties exist, so every variant is a separate component.", ""]
|
||||
out += ["## Components", ""]
|
||||
for g, names in C.GROUPS.items():
|
||||
out += [f"### {g}", "", "| Component (exact name) | Looks like | Text layers (override keys) |", "|---|---|---|"]
|
||||
for n in names:
|
||||
html = dict(C.SPECS)[n]
|
||||
keys = []
|
||||
for k, t in LEAF.findall(html):
|
||||
if k not in keys:
|
||||
keys.append(k)
|
||||
keys = [f"`{k}`" for k in keys]
|
||||
out.append(f"| `{n}` | {C.NOTES.get(n, '')} | {', '.join(keys) or '(none)'} |")
|
||||
out.append("")
|
||||
out += ["## Style-override tips", "",
|
||||
"- **Recolor an Avatar**: `style=\"background-color:var(--color-avatar-purple)\"` on the instance (palette: brand-primary, avatar-purple, avatar-teal, warning, avatar-pink, success, avatar-orange, avatar-indigo; the app picks by a hash of the name). Set initials with `{\"Initials\":\"PI\"}`.",
|
||||
"- **Active sidebar item**: use the variant `Admin Sidebar / <Item> Active` (Dashboard, Challenges, Question bank, Sessions, Schedule, Configuration, Rubric) or `Candidate Sidebar / Home Active` / `My interview Active`. Plain `Admin Sidebar` / `Candidate Sidebar` have no active item. Manual route (text/bg only; icon stays gray): `{\"Nav / Sessions\":{\"backgroundColor\":\"var(--color-brand-primary)\"},\"Nav Label / Sessions\":{\"color\":\"var(--color-text-inverse)\",\"fontWeight\":600}}`. User name/email: `User Name`, `User Email`, `User Initials`, `User Role Tag Label`.",
|
||||
"- **Score Bar**: set `Fill` width in px of the 160px track: `{\"Fill\":{\"width\":\"100px\"}}`; recolor `{\"Fill\":{\"backgroundColor\":\"var(--color-avatar-purple)\"}}`; thinner: `{\"Fill\":{\"height\":\"4px\"}}` plus root `style=\"height:4px\"`.",
|
||||
"- **Segmented / Ant Segmented**: pick the variant whose `Active N` matches; edit `Option 1..n`.",
|
||||
"- **Tabs / Bar**: pick `Active N` variant; edit `Tab N Label`. Bars have a 1px divider under them.",
|
||||
"- **Stat**: `Label` (uppercase), `Value`, `Delta`, `Trend`, `Delta Arrow` (use `Stat / Orange Down` for a negative delta). Sparkline shape is fixed.",
|
||||
"- **Pill / Tag / Badge status colors**: choose the tone component. Custom: `{\"Label\":{\"color\":\"var(--color-tag-red-fg)\"}}` and root `style=\"background-color:var(--color-tag-red-bg);border:1px solid var(--color-tag-red-bd)\"`.",
|
||||
"- **Data Table / Header Row, Row, Ant Table rows**: 5 equal flex cells `Col 1..5` / `Cell 1..5`; for fewer columns hide the trailing ones with display:none; width via `style`. For rows with Tags/Avatars compose raw HTML in a `<table>` and reuse `Tag / ...` instances in cells. Wrap a table in a Card-like frame: radius-lg, 1px border-light, overflow hidden.",
|
||||
"- **Inputs**: `Value` is the text (placeholder color by default; use `Input / Filled` for values). Width via instance style. `Form Field*` has `Label`, `Required Mark`, `Value`, `Help`.",
|
||||
"- **Disabled button**: `style=\"opacity:.5\"` on the instance. **Block button**: `style=\"width:100%\"` (add `justify-content:center`: already centered).",
|
||||
"- **Card with gradient** (dashboard Awaiting review): build raw: `background:linear-gradient(180deg,#fffdf5,#ffffff)`.", "",
|
||||
"## Recipes for raw HTML (when no component fits)", "",
|
||||
"- Card: `display:flex;flex-direction:column;padding:var(--spacing-lg);background:var(--color-bg-elevated);border:1px solid var(--color-border-light);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm)`.",
|
||||
"- Button with icon: row, gap 8px, height 34px (sm 28, lg 40), padding 0 16px, radius-md; icon 14-16px via `<x-paper-icon name=\"phosphor:plus\" size=\"16\" color=\"var(--color-text-inverse)\"/>`.",
|
||||
"- Page frame 1440x900: left `Admin Sidebar ...` (248x900) + main column 1192 wide: `Admin Shell Header` (about 102px tall) + content (padding 24, bg-layout, max-width 900/1100 centered).",
|
||||
"- Modal overlay: full-screen frame `background:var(--color-bg-overlay)` with a `Modal` instance centered.",
|
||||
"- Section titles inside cards: `Section Title` / `Section Title / With Action` instances.", "",
|
||||
"## Variables (use as `var(--name)`)", "",
|
||||
"Core tokens exist in the project (from tokens.css, same names minus the leading `--`): color-brand-primary, color-brand-secondary, color-bg-layout, color-bg-elevated, color-bg-sider, color-bg-selected, color-bg-overlay, color-border, color-border-light, color-border-strong, color-text-primary, color-text-secondary, color-text-disabled, color-text-inverse, color-text-link, color-success, color-warning, color-error, color-info, color-diff-added, color-diff-removed, color-diff-neutral, color-code-bg, color-code-fg; spacing-xs/sm/md/lg/xl/xxl (4/8/16/24/32/48), page-padding (24), page-padding-lg (48), page-max-width-xs/sm/md/lg/xl/narrow/default/wide (480/420/720/900/1100/720/900/1100), font-size-xs..xxl (12/14/16/18/20/24), radius-sm/md/lg (4/8/12), shadow-sm/md/lg, z-base/dropdown/modal/toast (1/1000/1050/1100).", "",
|
||||
"Note: multi-value shorthands (`padding:var(--spacing-md) var(--page-padding)`) are NOT resolved by write_html: use single-value padding with var, or literal px for shorthands. Single `var(--spacing-*)`, `var(--radius-*)`, `var(--shadow-*)`, `var(--font-size-*)` work.", "",
|
||||
"### Extra color variables added by the Design System page", ""]
|
||||
for g, names in COLOR_GROUPS.items():
|
||||
ex = [n for n in names if n in EXTRA_VARS]
|
||||
if ex:
|
||||
out.append(f"- **{g}**: " + ", ".join(f"`{n}` {EXTRA_VARS[n]}" for n in ex))
|
||||
out += ["", "### Font stack and type scale", "",
|
||||
"Inter only (regular/bold; weights 500 render regular, 600+ bold). Sizes: 12 labels/tags/table headers, 14 body, 16 section/card title, 24 page title (600), 30 stat value (700), 15/16 brand name.", "",
|
||||
"## Known deviations from the source", "",
|
||||
"- Pill in the code is the filter chip (active/inactive); there are no tone variants (`Pill / Success` does not exist). Tones are `Tag`.",
|
||||
"- `lint_design`: remaining errors/warnings are faithful brand colors (white on #1677ff 4.1:1, success green on white, etc.), control heights under 44px (34/32px as in the source), and svg-internal names. Not changed on purpose.",
|
||||
"- Text under 12px in the source (10.5/11) is rendered at 12; padding off the 4px grid was snapped (11->12, 18->16, 14->16, 9->0 with fixed 36px item height).",
|
||||
"- Main components cannot be nested in the `Section / Components` frame (create_component/import_components need top-level frames), so the section shows instances; masters are in the library area.", ""]
|
||||
open("DESIGN_SYSTEM.md", "w").write("\n".join(out))
|
||||
print(len(out))
|
||||
@@ -0,0 +1,8 @@
|
||||
import sys; sys.path.insert(0,"/Volumes/Storage/Projects/papercraft/examples/interview")
|
||||
from ds_common import *
|
||||
fid,P=setup()
|
||||
out="/private/tmp/claude-501/-Volumes-Storage-Projects-photocraft/11f4040a-b4f4-4b9e-965a-bde08b400450/scratchpad/"
|
||||
want=sys.argv[1:]
|
||||
for c in P.c("get_children",nodeId="root")["children"]:
|
||||
if c["name"] in want or (want==["all"]):
|
||||
print(c["name"],c["width"],c["height"]); P.screenshot(out+c["name"].replace(" ","_").replace("/","")+".png",c["id"],1)
|
||||
Reference in New Issue
Block a user