ultimate-jekyll-manager 0.0.304 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CLAUDE.md +140 -1
  3. package/README.md +1 -1
  4. package/TODO.md +1 -1
  5. package/dist/assets/css/pages/admin/calendar/index.scss +477 -0
  6. package/dist/assets/css/pages/admin/dashboard/index.scss +7 -0
  7. package/dist/assets/css/pages/admin/firebase/index.scss +33 -0
  8. package/dist/assets/css/pages/admin/users/index.scss +9 -0
  9. package/dist/assets/css/pages/feedback/index.scss +31 -0
  10. package/dist/assets/js/core/auth.js +20 -1
  11. package/dist/assets/js/libs/admin-helpers.js +54 -0
  12. package/dist/assets/js/libs/form-manager.js +42 -0
  13. package/dist/assets/js/modules/vert.js +1 -1
  14. package/dist/assets/js/pages/account/index.js +19 -21
  15. package/dist/assets/js/pages/account/sections/billing.js +37 -76
  16. package/dist/assets/js/pages/account/sections/connections.js +10 -10
  17. package/dist/assets/js/pages/account/sections/refund.js +3 -5
  18. package/dist/assets/js/pages/account/test-subscriptions/active.js +5 -0
  19. package/dist/assets/js/pages/account/test-subscriptions/cancellation-requested.js +5 -0
  20. package/dist/assets/js/pages/account/test-subscriptions/cancelled.js +12 -0
  21. package/dist/assets/js/pages/account/test-subscriptions/suspended.js +12 -0
  22. package/dist/assets/js/pages/account/test-subscriptions/trialing.js +5 -0
  23. package/dist/assets/js/pages/admin/calendar/calendar-core.js +313 -0
  24. package/dist/assets/js/pages/admin/calendar/calendar-events.js +213 -0
  25. package/dist/assets/js/pages/admin/calendar/calendar-renderer.js +661 -0
  26. package/dist/assets/js/pages/admin/calendar/index.js +70 -0
  27. package/dist/assets/js/pages/admin/dashboard/index.js +459 -0
  28. package/dist/assets/js/pages/admin/firebase/index.js +740 -0
  29. package/dist/assets/js/pages/admin/notifications/index.js +53 -0
  30. package/dist/assets/js/pages/admin/notifications/new/index.js +1 -1
  31. package/dist/assets/js/pages/admin/users/index.js +432 -0
  32. package/dist/assets/js/pages/admin/users/new/index.js +67 -0
  33. package/dist/assets/js/pages/feedback/index.js +204 -0
  34. package/dist/assets/js/pages/payment/checkout/index.js +23 -13
  35. package/dist/assets/js/pages/payment/checkout/modules/api.js +4 -4
  36. package/dist/assets/js/pages/payment/checkout/modules/state.js +1 -1
  37. package/dist/assets/js/pages/pricing/index.js +23 -0
  38. package/dist/assets/themes/bootstrap/overrides/_buttons-adaptive.scss +6 -0
  39. package/dist/assets/themes/classy/_theme.scss +1 -0
  40. package/dist/assets/themes/classy/css/components/_buttons.scss +6 -0
  41. package/dist/assets/themes/classy/css/layout/_backend.scss +59 -0
  42. package/dist/assets/themes/classy/css/layout/_navigation.scss +11 -0
  43. package/dist/defaults/dist/_includes/admin/sections/sidebar.json +32 -17
  44. package/dist/defaults/dist/_includes/core/body.html +7 -0
  45. package/dist/defaults/dist/_includes/core/foot.html +83 -0
  46. package/dist/defaults/dist/_includes/core/head.html +7 -1
  47. package/dist/defaults/dist/_includes/themes/classy/backend/sections/sidebar.html +92 -8
  48. package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +311 -0
  49. package/dist/defaults/dist/_layouts/blueprint/admin/dashboard/index.html +183 -187
  50. package/dist/defaults/dist/_layouts/blueprint/admin/firebase/index.html +162 -221
  51. package/dist/defaults/dist/_layouts/blueprint/admin/newsletters/index.html +59 -0
  52. package/dist/defaults/dist/_layouts/blueprint/admin/newsletters/new.html +46 -0
  53. package/dist/defaults/dist/_layouts/blueprint/admin/notifications/index.html +62 -238
  54. package/dist/defaults/dist/_layouts/blueprint/admin/notifications/new.html +16 -1
  55. package/dist/defaults/dist/_layouts/blueprint/admin/stackblitz/index.html +33 -0
  56. package/dist/defaults/dist/_layouts/blueprint/admin/users/index.html +193 -154
  57. package/dist/defaults/dist/_layouts/blueprint/admin/users/new.html +57 -123
  58. package/dist/defaults/dist/_layouts/blueprint/alternatives/alternative.html +11 -0
  59. package/dist/defaults/dist/_layouts/blueprint/contact.html +5 -0
  60. package/dist/defaults/dist/_layouts/blueprint/download.html +11 -0
  61. package/dist/defaults/dist/_layouts/blueprint/extension/index.html +5 -0
  62. package/dist/defaults/dist/_layouts/blueprint/feedback.html +12 -0
  63. package/dist/defaults/dist/_layouts/blueprint/index.html +9 -0
  64. package/dist/defaults/dist/_layouts/blueprint/pricing.html +11 -0
  65. package/dist/defaults/dist/_layouts/core/root.html +1 -0
  66. package/dist/defaults/dist/_layouts/themes/classy/admin/core/{base.html → minimal-viewport-locked.html} +3 -1
  67. package/dist/defaults/dist/_layouts/themes/classy/admin/core/minimal.html +2 -0
  68. package/dist/defaults/dist/_layouts/themes/classy/backend/core/base.html +1 -1
  69. package/dist/defaults/dist/_layouts/themes/classy/backend/core/minimal-viewport-locked.html +149 -0
  70. package/dist/defaults/dist/_layouts/themes/classy/backend/core/minimal.html +1 -1
  71. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +2 -2
  72. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/feedback.html +201 -0
  73. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/index.html +1 -0
  74. package/dist/defaults/dist/pages/admin/calendar/index.html +7 -0
  75. package/dist/defaults/dist/pages/admin/index.md +3 -1
  76. package/dist/defaults/dist/pages/admin/newsletters/index.html +7 -0
  77. package/dist/defaults/dist/pages/admin/newsletters/new.html +7 -0
  78. package/dist/defaults/dist/pages/admin/stackblitz/index.html +7 -0
  79. package/dist/defaults/dist/pages/feedback.md +7 -0
  80. package/dist/defaults/dist/redirects/authentication/account.html +1 -1
  81. package/dist/defaults/dist/redirects/authentication/authentication-required.html +1 -1
  82. package/dist/defaults/dist/redirects/authentication/authentication-success.html +1 -1
  83. package/dist/defaults/dist/redirects/authentication/authentication-token.html +1 -1
  84. package/dist/defaults/dist/redirects/authentication/forgot.html +1 -1
  85. package/dist/defaults/dist/redirects/authentication/helpers/cancel.html +1 -1
  86. package/dist/defaults/dist/redirects/authentication/helpers/change-password.html +1 -1
  87. package/dist/defaults/dist/redirects/authentication/helpers/forgot.html +1 -1
  88. package/dist/defaults/dist/redirects/authentication/helpers/join.html +1 -1
  89. package/dist/defaults/dist/redirects/authentication/helpers/login.html +1 -1
  90. package/dist/defaults/dist/redirects/authentication/helpers/recover.html +1 -1
  91. package/dist/defaults/dist/redirects/authentication/helpers/refund.html +1 -1
  92. package/dist/defaults/dist/redirects/authentication/helpers/register.html +1 -1
  93. package/dist/defaults/dist/redirects/authentication/helpers/reset-password.html +1 -1
  94. package/dist/defaults/dist/redirects/authentication/oauth2.html +1 -1
  95. package/dist/defaults/dist/redirects/authentication/signin.html +1 -1
  96. package/dist/defaults/dist/redirects/authentication/signout.html +1 -1
  97. package/dist/defaults/dist/redirects/authentication/signup.html +1 -1
  98. package/dist/defaults/dist/redirects/download/all.html +1 -1
  99. package/dist/defaults/dist/redirects/extension/browser-extension.html +1 -1
  100. package/dist/defaults/dist/redirects/legal/cookie-policy.html +1 -1
  101. package/dist/defaults/dist/redirects/legal/privacy-policy.html +1 -1
  102. package/dist/defaults/dist/redirects/legal/terms-of-service.html +1 -1
  103. package/dist/defaults/dist/redirects/legal/tos.html +1 -1
  104. package/dist/defaults/src/_config.yml +11 -0
  105. package/dist/service-worker.js +2 -2
  106. package/package.json +12 -10
  107. package/dist/assets/js/pages/admin/index.js +0 -492
  108. package/dist/defaults/dist/_layouts/blueprint/admin/index.html +0 -30
  109. package/dist/defaults/dist/_layouts/themes/classy/admin/core/iframe.html +0 -70
  110. package/dist/defaults/dist/redirects/admin/admin.html +0 -9
package/CHANGELOG.md CHANGED
@@ -16,6 +16,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
16
16
 
17
17
  ---
18
18
  ## [Unreleased]
19
+ ### Changed
20
+ - Migrate "app" terminology to "brand" across frontend and service worker: renamed `appData`/`fetchAppData` to `brandData`/`fetchBrandData`, `appConfig`/`fetchAppConfig` to `brandConfig`/`fetchBrandConfig`, API endpoint from `/backend-manager/app` to `/backend-manager/brand`, and `this.app` to `this.brand` in service worker
21
+
19
22
  ### Added
20
23
  - Abandoned cart tracking on checkout page: creates a Firestore document in `payments-carts/{uid}` when authenticated users begin checkout, with a 15-minute first reminder delay
21
24
  - Backend sidebar auto-expands collapsible dropdown sections containing the currently active page link (desktop and mobile)
@@ -27,8 +30,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
27
30
  - Auth state settles before any authorized fetches fire on checkout, preventing race conditions
28
31
  - Quick boot mode (`UJ_QUICK=true`) for faster dev server startup (~5s vs ~20s) by skipping clean, slow setup operations, and deferring webpack/sass compilation until after Jekyll's first build
29
32
  - Dev-only warning in FormManager for form fields missing `name` attributes (skipped by validation and `getData()`)
33
+ - FAQPage JSON-LD schema with 3-level fallback chain (`schema.faq_page.items` → `faqs.items` → `alternative.faqs.items`)
34
+ - FAQPage schema enabled on blueprint pages with FAQ sections (pricing, contact, download, extension, alternatives)
35
+ - OG image dimension meta tags (`og:image:width`, `og:image:height`) with 1200×630 defaults
36
+ - Article published/modified time meta tags for blog posts
37
+ - Admin marketing calendar page (`/admin/calendar`) with custom-built interactive calendar for scheduling newsletters and notifications
38
+ - Calendar supports 4 view modes (month, week, day, year) with event CRUD, drag-and-drop, overlapping event layout, and `window.calendarAPI`
39
+ - Real-time red "now" line indicator in day/week views, updates every 60 seconds
40
+ - Viewport-locked admin layout variant (`themes/classy/admin/core/minimal-viewport-locked`) for full-height admin pages
41
+ - Feedback page (`/feedback`) with emoji rating selection, written feedback fields, review prompt modal, and analytics tracking
42
+ - FormManager auto-populates form fields from URL query parameters (skips utm_*, itm_*, cb, fbclid, gclid)
43
+ - Review prompt modal after positive feedback submission with copy-paste textarea and external review site link
30
44
 
31
45
  ### Changed
46
+ - Twitter card default from `summary` to configurable `summary_large_image`
32
47
  - Rename `site.tracking` config to `site.analytics` with simplified keys (`google-analytics` → `google`, `meta-pixel` → `meta`, `tiktok-pixel` → `tiktok`)
33
48
  - Update `webManager.config.tracking['meta-pixel']` to `webManager.config.analytics?.meta` in auth.js
34
49
  - Replace hardcoded discount codes with server-side validation via `payments/discount` API endpoint
@@ -58,6 +73,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
58
73
  - Fix admin forms (notifications, users) and blog/status forms missing `novalidate`, `onsubmit`, `name` attributes, and `.button-text` spans
59
74
  - Fix profile premium badge using removed `trialing` status and `access` field
60
75
  - Add dev-only artificial pre-delay support to checkout page for testing form protection timing
76
+ - Fix `btn-check:checked` outline button styling in classy theme — transparent `!important` rule was overriding Bootstrap's checked background due to higher CSS specificity
61
77
 
62
78
  ---
63
79
  ## [1.0.0] - 2024-06-19
package/CLAUDE.md CHANGED
@@ -363,7 +363,7 @@ Ultimate Jekyll uses the `jekyll-uj-powertools` gem for custom Liquid functional
363
363
  **Documentation:** `/Users/ian/Developer/Repositories/ITW-Creative-works/jekyll-uj-powertools/README.md`
364
364
 
365
365
  ### Available Features
366
- - **Filters:** `uj_strip_ads`, `uj_json_escape`, `uj_title_case`, `uj_content_format`
366
+ - **Filters:** `uj_strip_ads`, `uj_json_escape`, `uj_title_case`, `uj_content_format`, `uj_hash`
367
367
  - **Tags:** `iftruthy`, `iffalsy`, `uj_icon`, `uj_logo`, `uj_image`, `uj_member`, `uj_post`, `uj_readtime`, `uj_social`, `uj_translation_url`, `uj_fake_comments`, `uj_language`
368
368
  - **Global Variables:** `site.uj.cache_breaker`
369
369
  - **Page Variables:** `page.random_id`, `page.extension`, `page.layout_data`, `page.resolved`
@@ -375,6 +375,14 @@ Ultimate Jekyll uses the `jekyll-uj-powertools` gem for custom Liquid functional
375
375
  #### `uj_content_format`
376
376
  Formats content by first liquifying it, then markdownifying it (if markdown file).
377
377
 
378
+ #### `uj_hash`
379
+ Returns a deterministic number between 0 and max (exclusive) based on the input string's MD5 hash. Same input always produces the same output.
380
+
381
+ ```liquid
382
+ {{ "some-string" | uj_hash: 1000 }} => 0-999 (stable across builds)
383
+ {{ site.url | uj_hash: 2 }} => 0 or 1
384
+ ```
385
+
378
386
  #### `iftruthy` / `iffalsy`
379
387
  Custom tags that check JavaScript truthiness (not null, undefined, or empty string).
380
388
 
@@ -481,6 +489,12 @@ When posts are created via BEM's `POST /admin/post` endpoint:
481
489
 
482
490
  Ultimate Jekyll uses Font Awesome icons but does NOT include the Font Awesome JavaScript or CSS library. All icons must be rendered server-side using Jekyll's `{% uj_icon %}` tag.
483
491
 
492
+ ### Available Icons
493
+
494
+ UJM ships with the **full Font Awesome Pro solid icon set** (4,600+ icons) at `assets/icons/font-awesome/solid/`, plus brand icons at `assets/icons/font-awesome/brands/`. Any Pro or Free solid/brand icon name can be used with `{% uj_icon %}` and prerendered icons. The icon style defaults to `solid` and can be configured via `site.config.icons.style`.
495
+
496
+ Browse available icons at: https://fontawesome.com/icons
497
+
484
498
  ### When to Use `{% uj_icon %}` vs Prerendered Icons
485
499
 
486
500
  **IMPORTANT:** Use the correct method based on WHERE the icon will be used:
@@ -911,6 +925,26 @@ Custom library for site management functionality.
911
925
 
912
926
  **Important:** Always check the source code or README before assuming a function exists. Do not guess at API methods.
913
927
 
928
+ #### Subscription Resolution
929
+
930
+ Use `webManager.auth().resolveSubscription(account)` to derive calculated subscription state. This is the **single source of truth** for determining a user's effective plan — do NOT manually check `subscription.status`, `trial.claimed`, or `cancellation.pending` separately.
931
+
932
+ ```javascript
933
+ const resolved = webManager.auth().resolveSubscription(account);
934
+ // Returns: { plan, active, trialing, cancelling }
935
+ ```
936
+
937
+ | Field | Description |
938
+ |-------|-------------|
939
+ | `plan` | Effective plan ID right now (`'basic'` if cancelled/suspended) |
940
+ | `active` | Has active access (active, trialing, or cancelling) |
941
+ | `trialing` | In active trial |
942
+ | `cancelling` | Cancellation pending |
943
+
944
+ Raw subscription data (product.id, status, trial, cancellation) is on `account.subscription` directly — `resolveSubscription()` returns only the calculated/derived fields.
945
+
946
+ The same function exists in BEM as `User.resolveSubscription(account)` with identical return shape.
947
+
914
948
  ### Ultimate Jekyll Libraries
915
949
 
916
950
  Ultimate Jekyll provides helper libraries in `src/assets/js/libs/` that can be imported as needed.
@@ -1348,6 +1382,111 @@ Which renders as "Can I import my data from ExampleApp?" for an ExampleApp compe
1348
1382
  | CSS | `src/assets/css/pages/alternatives/alternative/index.scss` |
1349
1383
  | JS | `src/assets/js/pages/alternatives/alternative/index.js` |
1350
1384
 
1385
+ ## Schema / Structured Data (JSON-LD)
1386
+
1387
+ UJ automatically generates JSON-LD structured data in `foot.html`. The SoftwareApplication schema with AggregateRating is opt-in via frontmatter.
1388
+
1389
+ ### SoftwareApplication Schema
1390
+
1391
+ Renders a `SoftwareApplication` JSON-LD block with deterministic aggregate ratings. Enabled by blueprint layouts (index, pricing, download, alternatives/alternative) — consuming projects can override or disable per page.
1392
+
1393
+ **How it works:**
1394
+
1395
+ 1. **`_config.yml`** sets fallback defaults (no `enabled` key — just field defaults like `application_category`, `price`, etc.)
1396
+ 2. **Blueprint layouts** set `schema.software_application.enabled: true` with page-appropriate `features`
1397
+ 3. **Consuming projects** can override any value in their page frontmatter, or disable with `enabled: false`
1398
+
1399
+ This follows the standard `page.resolved` merge: page > layout > site.
1400
+
1401
+ **Deterministic ratings:** Uses the `uj_hash` filter (from jekyll-uj-powertools) seeded with `site.url` by default, producing stable values across builds:
1402
+ - Rating: always `4.8` or `4.9` (deterministic per seed)
1403
+ - Review count: 200,000–999,999 (deterministic per seed)
1404
+ - Override seed per page with `hash_seed` to get different values
1405
+
1406
+ **Blueprint frontmatter example:**
1407
+ ```yaml
1408
+ ### SCHEMA ###
1409
+ schema:
1410
+ software_application:
1411
+ enabled: true
1412
+ features:
1413
+ - "Free to use"
1414
+ - "24/7 availability"
1415
+ - "User-friendly interface"
1416
+ ```
1417
+
1418
+ **Consuming project override example:**
1419
+ ```yaml
1420
+ schema:
1421
+ software_application:
1422
+ application_category: "EducationalApplication"
1423
+ features:
1424
+ - "AI-powered solutions"
1425
+ - "24/7 availability"
1426
+ ```
1427
+
1428
+ **Consuming project disable example:**
1429
+ ```yaml
1430
+ schema:
1431
+ software_application:
1432
+ enabled: false
1433
+ ```
1434
+
1435
+ **Available fields:**
1436
+
1437
+ | Field | Default | Description |
1438
+ |-------|---------|-------------|
1439
+ | `enabled` | (set by blueprint) | Enable/disable the schema block |
1440
+ | `name` | `site.brand.name` | Application name |
1441
+ | `description` | `page.resolved.meta.description` | Application description |
1442
+ | `application_category` | `WebApplication` | Schema.org application category |
1443
+ | `operating_system` | `Web-based` | Target OS |
1444
+ | `price` | `0` | Price (string) |
1445
+ | `price_currency` | `USD` | Currency code |
1446
+ | `features` | `[]` | Feature list for `featureList` field |
1447
+ | `hash_seed` | `site.url` | Seed for deterministic rating/count generation |
1448
+
1449
+ **File locations:**
1450
+
1451
+ | Purpose | Path |
1452
+ |---------|------|
1453
+ | Schema block (rendering) | `src/defaults/dist/_includes/core/foot.html` |
1454
+ | Site-level defaults | `src/defaults/src/_config.yml` (under `schema:`) |
1455
+ | Blueprint activation | `src/defaults/dist/_layouts/blueprint/{index,pricing,download}.html`, `blueprint/alternatives/alternative.html` |
1456
+ | Hash filter | `jekyll-uj-powertools/lib/filters/main.rb` (`uj_hash`) |
1457
+
1458
+ ### FAQPage Schema
1459
+
1460
+ Renders a `FAQPage` JSON-LD block for pages with FAQ/accordion sections. Enabled by the alternatives blueprint — consuming projects can also enable it on any page with FAQ content.
1461
+
1462
+ **How it works:**
1463
+
1464
+ 1. **`_config.yml`** sets `faq_page.items: []` as fallback
1465
+ 2. **Blueprint layouts** set `schema.faq_page.enabled: true`
1466
+ 3. **Items source (fallback chain):** `schema.faq_page.items` → `page.resolved.faqs.items` → `page.resolved.alternative.faqs.items`. Pages with generic `faqs.items` (like pricing) and alternatives pages both get FAQPage schema automatically without duplicating content
1467
+ 4. Questions/answers are processed through `uj_liquify` (supports Liquid expressions like competitor names) and `uj_json_escape`
1468
+
1469
+ **Blueprint activation:** Enabled by default in `blueprint/pricing.html`, `blueprint/contact.html`, `blueprint/download.html`, `blueprint/extension/index.html`, and `blueprint/alternatives/alternative.html`.
1470
+
1471
+ **Consuming project usage — provide items directly:**
1472
+ ```yaml
1473
+ schema:
1474
+ faq_page:
1475
+ enabled: true
1476
+ items:
1477
+ - question: "How do I get started?"
1478
+ answer: "Sign up for free and follow the onboarding guide."
1479
+ - question: "Is there a free plan?"
1480
+ answer: "Yes, our basic plan is completely free."
1481
+ ```
1482
+
1483
+ **Available fields:**
1484
+
1485
+ | Field | Default | Description |
1486
+ |-------|---------|-------------|
1487
+ | `enabled` | (set by blueprint) | Enable/disable the schema block |
1488
+ | `items` | `[]` | Array of `{question, answer}` objects. Falls back to `alternative.faqs.items` if empty |
1489
+
1351
1490
  ## Audit Workflow
1352
1491
 
1353
1492
  When fixing issues identified by the audit task (`src/gulp/tasks/audit.js`):
package/README.md CHANGED
@@ -385,7 +385,7 @@ Raw pixel values also accepted: `vert-size="300"` → 300px max-height. Omit `ve
385
385
  - Inserts fake session data in the Security section (active sessions)
386
386
 
387
387
  ##### Checkout Page (`/payment/checkout`)
388
- * `_dev_appId`: Override the application ID for testing (e.g., `_dev_appId=test-app`)
388
+ * `_dev_brandId`: Override the brand ID for testing (e.g., `_dev_brandId=test-app`)
389
389
  * `_dev_trialEligible`: Force trial eligibility status:
390
390
  - `_dev_trialEligible=true`: User is eligible for trial
391
391
  - `_dev_trialEligible=false`: User is not eligible for trial
package/TODO.md CHANGED
@@ -38,7 +38,7 @@ MAKE COMPONENTS? ASK CLAUDE HOW AND IF ITS POSSIBLE
38
38
 
39
39
  FIX SIGNIN/SINGUP FORM
40
40
 
41
- https://192.168.86.69:4000/payment/checkout?product=premium&_dev_trialEligible=true&_dev_appId=dev&_dev_cardProcessor=stripe
41
+ https://192.168.86.69:4000/payment/checkout?product=premium&_dev_trialEligible=true&_dev_brandId=dev&_dev_cardProcessor=stripe
42
42
 
43
43
  filter-adaptive-inverse
44
44
 
@@ -0,0 +1,477 @@
1
+ // Admin Calendar Styles
2
+
3
+ // Current time / today accent color
4
+ $calendar-now-color: #F44336;
5
+
6
+ // ============================================
7
+ // Fix: viewport-locked layout sets display:flex on all direct children,
8
+ // which overrides Bootstrap modal's default display:none.
9
+ // Restore Bootstrap's default: hidden unless .show is present.
10
+ // ============================================
11
+ #calendar-event-modal:not(.show) {
12
+ display: none !important;
13
+ }
14
+
15
+ // ============================================
16
+ // Calendar Root
17
+ // ============================================
18
+ #calendar-root {
19
+ min-height: 0;
20
+ }
21
+
22
+ // ============================================
23
+ // Toolbar
24
+ // ============================================
25
+ #calendar-toolbar {
26
+ gap: 0.5rem;
27
+ flex-wrap: wrap;
28
+ }
29
+
30
+ .calendar-toolbar-nav {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: 0.25rem;
34
+ }
35
+
36
+ .calendar-toolbar-period {
37
+ font-size: 1.1rem;
38
+ font-weight: 600;
39
+ text-align: center;
40
+ min-width: 200px;
41
+ }
42
+
43
+ .calendar-toolbar-views .btn {
44
+ padding: 0.25rem 0.75rem;
45
+ font-size: 0.8rem;
46
+ }
47
+
48
+ // ============================================
49
+ // Calendar Grid (shared)
50
+ // ============================================
51
+ #calendar-grid {
52
+ display: flex;
53
+ flex-direction: column;
54
+ min-height: 0;
55
+ }
56
+
57
+ // Day-of-week header
58
+ .calendar-day-header {
59
+ display: grid;
60
+ grid-template-columns: repeat(7, 1fr);
61
+ text-align: center;
62
+ font-weight: 600;
63
+ font-size: 0.75rem;
64
+ text-transform: uppercase;
65
+ color: var(--bs-secondary-color);
66
+ padding: 0.5rem 0;
67
+ border-bottom: 1px solid var(--bs-border-color);
68
+ flex-shrink: 0;
69
+ }
70
+
71
+ // ============================================
72
+ // Month View
73
+ // ============================================
74
+ .calendar-month {
75
+ display: grid;
76
+ grid-template-columns: repeat(7, 1fr);
77
+ grid-template-rows: repeat(auto-fill, 1fr);
78
+ flex-grow: 1;
79
+ min-height: 0;
80
+ }
81
+
82
+ .calendar-cell {
83
+ border-right: 1px solid var(--bs-border-color);
84
+ border-bottom: 1px solid var(--bs-border-color);
85
+ padding: 0.25rem;
86
+ overflow-y: auto;
87
+ overflow-x: hidden;
88
+ min-height: 80px;
89
+ position: relative;
90
+ cursor: pointer;
91
+ transition: background-color 0.15s;
92
+
93
+ &:nth-child(7n) {
94
+ border-right: none;
95
+ }
96
+
97
+ &:hover {
98
+ background-color: var(--bs-tertiary-bg);
99
+ }
100
+ }
101
+
102
+ .calendar-cell--today {
103
+ background-color: rgba($calendar-now-color, 0.05);
104
+
105
+ .calendar-cell-date {
106
+ background-color: $calendar-now-color;
107
+ color: #fff;
108
+ border-radius: 50%;
109
+ width: 1.5rem;
110
+ height: 1.5rem;
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ }
115
+ }
116
+
117
+ .calendar-cell--outside {
118
+ opacity: 0.35;
119
+ }
120
+
121
+ .calendar-cell--drag-over {
122
+ background-color: rgba(var(--bs-primary-rgb), 0.1) !important;
123
+ }
124
+
125
+ .calendar-cell-date {
126
+ font-size: 0.8rem;
127
+ font-weight: 500;
128
+ margin-bottom: 0.125rem;
129
+ display: inline-flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ min-width: 1.5rem;
133
+ min-height: 1.5rem;
134
+ }
135
+
136
+ .calendar-cell-events {
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: 1px;
140
+ }
141
+
142
+ .calendar-cell-more {
143
+ font-size: 0.7rem;
144
+ color: var(--bs-secondary-color);
145
+ cursor: pointer;
146
+ padding: 0 0.25rem;
147
+
148
+ &:hover {
149
+ color: var(--bs-primary);
150
+ }
151
+ }
152
+
153
+ // ============================================
154
+ // Week View
155
+ // ============================================
156
+ .calendar-week-header {
157
+ display: grid;
158
+ grid-template-columns: 50px repeat(7, 1fr);
159
+ border-bottom: 1px solid var(--bs-border-color);
160
+ flex-shrink: 0;
161
+ }
162
+
163
+ .calendar-week-header-cell {
164
+ text-align: center;
165
+ padding: 0.5rem 0.25rem;
166
+ font-size: 0.75rem;
167
+ font-weight: 600;
168
+ text-transform: uppercase;
169
+ color: var(--bs-secondary-color);
170
+
171
+ .calendar-week-header-date {
172
+ display: block;
173
+ font-size: 1.25rem;
174
+ font-weight: 700;
175
+ color: var(--bs-body-color);
176
+ }
177
+
178
+ &.calendar-cell--today .calendar-week-header-date {
179
+ background-color: $calendar-now-color;
180
+ color: #fff;
181
+ border-radius: 50%;
182
+ width: 2rem;
183
+ height: 2rem;
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ margin: 0 auto;
188
+ }
189
+ }
190
+
191
+ .calendar-week-allday {
192
+ display: grid;
193
+ grid-template-columns: 50px repeat(7, 1fr);
194
+ border-bottom: 1px solid var(--bs-border-color);
195
+ min-height: 1.5rem;
196
+ flex-shrink: 0;
197
+
198
+ .calendar-week-time-label {
199
+ font-size: 0.65rem;
200
+ color: var(--bs-secondary-color);
201
+ padding: 0.125rem 0.25rem;
202
+ text-align: right;
203
+ }
204
+ }
205
+
206
+ .calendar-week-body {
207
+ display: grid;
208
+ grid-template-columns: 50px repeat(7, 1fr);
209
+ flex-grow: 1;
210
+ overflow-y: auto;
211
+ min-height: 0;
212
+ }
213
+
214
+ .calendar-week-time-col {
215
+ display: flex;
216
+ flex-direction: column;
217
+ }
218
+
219
+ .calendar-week-time-label {
220
+ height: 60px;
221
+ font-size: 0.65rem;
222
+ color: var(--bs-secondary-color);
223
+ text-align: right;
224
+ padding-right: 0.5rem;
225
+ padding-top: 0;
226
+ transform: translateY(-0.4rem);
227
+ }
228
+
229
+ .calendar-week-day-col {
230
+ position: relative;
231
+ border-left: 1px solid var(--bs-border-color);
232
+ }
233
+
234
+ .calendar-week-time-slot {
235
+ height: 60px;
236
+ border-bottom: 1px solid var(--bs-border-color-translucent);
237
+ position: relative;
238
+
239
+ &:hover {
240
+ background-color: var(--bs-tertiary-bg);
241
+ }
242
+ }
243
+
244
+ // Now line (real-time red indicator)
245
+ .calendar-now-line {
246
+ position: absolute;
247
+ left: 0;
248
+ right: 0;
249
+ height: 2px;
250
+ background-color: $calendar-now-color;
251
+ z-index: 2;
252
+ pointer-events: none;
253
+
254
+ &::before {
255
+ content: '';
256
+ position: absolute;
257
+ left: -4px;
258
+ top: -4px;
259
+ width: 10px;
260
+ height: 10px;
261
+ border-radius: 50%;
262
+ background-color: $calendar-now-color;
263
+ }
264
+ }
265
+
266
+ .calendar-week-event {
267
+ position: absolute;
268
+ left: 2px;
269
+ right: 2px;
270
+ border-radius: 4px;
271
+ padding: 0.125rem 0.25rem;
272
+ font-size: 0.7rem;
273
+ overflow: hidden;
274
+ cursor: pointer;
275
+ z-index: 1;
276
+ color: #fff;
277
+ border-left: 3px solid rgba(0, 0, 0, 0.2);
278
+
279
+ &:hover {
280
+ opacity: 0.85;
281
+ }
282
+ }
283
+
284
+ // ============================================
285
+ // Day View
286
+ // ============================================
287
+ .calendar-day-header-single {
288
+ text-align: center;
289
+ padding: 0.5rem;
290
+ font-size: 1rem;
291
+ font-weight: 600;
292
+ border-bottom: 1px solid var(--bs-border-color);
293
+ flex-shrink: 0;
294
+ }
295
+
296
+ .calendar-day-body {
297
+ display: grid;
298
+ grid-template-columns: 50px 1fr;
299
+ flex-grow: 1;
300
+ overflow-y: auto;
301
+ min-height: 0;
302
+ }
303
+
304
+ .calendar-day-col {
305
+ position: relative;
306
+ }
307
+
308
+ // Day events reuse week event styles via shared class in renderer
309
+
310
+ // ============================================
311
+ // Year View
312
+ // ============================================
313
+ .calendar-year {
314
+ display: grid;
315
+ grid-template-columns: repeat(4, 1fr);
316
+ gap: 1rem;
317
+ padding: 0.5rem;
318
+ overflow-y: auto;
319
+ flex-grow: 1;
320
+ min-height: 0;
321
+ }
322
+
323
+ .calendar-mini-month {
324
+ cursor: pointer;
325
+ padding: 0.5rem;
326
+ border-radius: 0.5rem;
327
+ transition: background-color 0.15s;
328
+
329
+ &:hover {
330
+ background-color: var(--bs-tertiary-bg);
331
+ }
332
+ }
333
+
334
+ .calendar-mini-month-title {
335
+ text-align: center;
336
+ font-weight: 600;
337
+ font-size: 0.85rem;
338
+ margin-bottom: 0.25rem;
339
+ }
340
+
341
+ .calendar-mini-month-grid {
342
+ display: grid;
343
+ grid-template-columns: repeat(7, 1fr);
344
+ text-align: center;
345
+ font-size: 0.65rem;
346
+ gap: 1px;
347
+ }
348
+
349
+ .calendar-mini-day-header {
350
+ color: var(--bs-secondary-color);
351
+ font-weight: 600;
352
+ padding: 0.125rem 0;
353
+ }
354
+
355
+ .calendar-mini-day {
356
+ padding: 0.125rem 0;
357
+ border-radius: 50%;
358
+ position: relative;
359
+ aspect-ratio: 1;
360
+ display: flex;
361
+ align-items: center;
362
+ justify-content: center;
363
+
364
+ &.calendar-cell--today {
365
+ background-color: $calendar-now-color;
366
+ color: #fff;
367
+ opacity: 1;
368
+ }
369
+
370
+ &.calendar-cell--outside {
371
+ visibility: hidden;
372
+ }
373
+
374
+ &.has-events::after {
375
+ content: '';
376
+ position: absolute;
377
+ bottom: 0;
378
+ left: 50%;
379
+ transform: translateX(-50%);
380
+ width: 4px;
381
+ height: 4px;
382
+ border-radius: 50%;
383
+ background-color: $calendar-now-color;
384
+ }
385
+ }
386
+
387
+ // ============================================
388
+ // Event Pills (Month View)
389
+ // ============================================
390
+ .calendar-event {
391
+ display: flex;
392
+ align-items: center;
393
+ gap: 0.25rem;
394
+ padding: 0.0625rem 0.25rem;
395
+ border-radius: 3px;
396
+ font-size: 0.7rem;
397
+ line-height: 1.3;
398
+ white-space: nowrap;
399
+ overflow: hidden;
400
+ text-overflow: ellipsis;
401
+ cursor: pointer;
402
+ color: #fff;
403
+ transition: opacity 0.15s;
404
+
405
+ &:hover {
406
+ opacity: 0.85;
407
+ }
408
+ }
409
+
410
+ .calendar-event--dragging {
411
+ opacity: 0.4 !important;
412
+ }
413
+
414
+ // During drag, disable pointer-events on all events so drops land on time slots
415
+ .calendar-grid--dragging {
416
+ .calendar-event,
417
+ .calendar-week-event {
418
+ pointer-events: none;
419
+ }
420
+ }
421
+
422
+ .calendar-event-time {
423
+ font-weight: 600;
424
+ font-size: 0.65rem;
425
+ flex-shrink: 0;
426
+ }
427
+
428
+ .calendar-event-title {
429
+ overflow: hidden;
430
+ text-overflow: ellipsis;
431
+ }
432
+
433
+ // ============================================
434
+ // Color Swatches
435
+ // ============================================
436
+ .color-swatch {
437
+ width: 28px;
438
+ height: 28px;
439
+ border-radius: 50%;
440
+ border: 2px solid transparent;
441
+ cursor: pointer;
442
+ transition: transform 0.15s, border-color 0.15s;
443
+ padding: 0;
444
+
445
+ &:hover {
446
+ transform: scale(1.15);
447
+ }
448
+
449
+ &.active {
450
+ border-color: var(--bs-body-color);
451
+ transform: scale(1.15);
452
+ }
453
+ }
454
+
455
+ // ============================================
456
+ // Responsive
457
+ // ============================================
458
+ @media (max-width: 767.98px) {
459
+ .calendar-year {
460
+ grid-template-columns: repeat(2, 1fr);
461
+ }
462
+
463
+ .calendar-toolbar-period {
464
+ min-width: auto;
465
+ font-size: 0.95rem;
466
+ }
467
+
468
+ .calendar-cell {
469
+ min-height: 60px;
470
+ }
471
+ }
472
+
473
+ @media (max-width: 575.98px) {
474
+ .calendar-year {
475
+ grid-template-columns: repeat(1, 1fr);
476
+ }
477
+ }