studio-engine 0.82.1 → 0.84.0

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.
data/lib/studio.rb CHANGED
@@ -13,6 +13,8 @@ require "studio/js_literal"
13
13
  require "studio/js_identifier"
14
14
  require "studio/partial_block"
15
15
  require "studio/sidebar_sections"
16
+ require "studio/site_footer"
17
+ require "studio/booking"
16
18
  require "studio/navbar_links"
17
19
  require "studio/navbar_identity"
18
20
  require "studio/profile_sections"
@@ -558,6 +560,119 @@ module Studio
558
560
  # config.draw_public_user_routes = true
559
561
  mattr_accessor :draw_public_user_routes, default: false
560
562
 
563
+ # ---- Site footer and booking (Studio::SiteFooter, Studio::Booking) -------
564
+ #
565
+ # The public site's footer: brand and social profiles, link columns, an
566
+ # optional Location band with a live map, and a legal line. And the booking
567
+ # primitives: Google Calendar's appointment page inline, in a popup, and on a
568
+ # page. See docs/SITE_FOOTER.md and docs/BOOKING.md.
569
+
570
+ # The footer's facts: a Hash, or a callable receiving the view. nil (the
571
+ # default) means this app has no footer, and `studio_site_footer` renders
572
+ # nothing, so a layout can call it before the app declares one. Every key is
573
+ # optional; shape and rules: lib/studio/site_footer.rb.
574
+ #
575
+ # config.site_footer = ->(view) {
576
+ # { tagline: "Everything, by example",
577
+ # address: { street: "123 Example St", city_line: "Washington, DC 20024",
578
+ # lat: 38.8894, lng: -77.0352 },
579
+ # columns: [ [ "Company", [ [ "Home", view.root_path ], [ "Career", nil ] ] ] ],
580
+ # legal: [ [ "Privacy Policy", view.privacy_path ] ] }
581
+ # }
582
+ mattr_reader :site_footer, default: nil
583
+
584
+ def self.site_footer=(declared)
585
+ SiteFooter.validate!(declared)
586
+ @@site_footer = declared
587
+ end
588
+
589
+ # Controllers whose pages keep the footer for a SIGNED-IN viewer, by
590
+ # controller name ("landing") or path ("admin/reports"). A visitor gets the
591
+ # footer on every page; a signed-in viewer gets it only here, because every
592
+ # other signed-in page is a working surface (a board, a queue, an editor) and
593
+ # stays full height. The booking page (the engine's, or the app's own at
594
+ # Studio.booking_path) is always included.
595
+ #
596
+ # config.site_footer_controllers = %w[landing packages contact_submissions]
597
+ mattr_accessor :site_footer_controllers, default: []
598
+
599
+ # THE WHOLE RULE for where the footer shows, as one callable receiving the
600
+ # view. The default is the rule above: every page for a visitor, the listed
601
+ # controllers for a signed-in viewer. That default shows the footer on ANY page
602
+ # a signed-out visitor can reach, an app page included, so an app with public
603
+ # working pages narrows it here. Compose with the default rather than
604
+ # restating it:
605
+ #
606
+ # config.site_footer_visible = ->(view) {
607
+ # Studio::SiteFooter.default_visible?(view) && !view.controller_path.start_with?("games/")
608
+ # }
609
+ mattr_reader :site_footer_visible, default: ->(view) { Studio::SiteFooter.default_visible?(view) }
610
+
611
+ def self.site_footer_visible=(rule)
612
+ SiteFooter.validate_visible!(rule)
613
+ @@site_footer_visible = rule
614
+ end
615
+
616
+ # The app's Google Calendar appointment schedule: the public link Google gives
617
+ # you to share, WITHOUT `?gv=true` (the frame and the popup add it). nil (the
618
+ # default) means no booking: `studio_booking_frame` and `studio_booking_popup`
619
+ # render nothing and no link opens a popup.
620
+ #
621
+ # config.booking_url = "https://calendar.google.com/calendar/appointments/schedules/EXAMPLE-SCHEDULE-ID"
622
+ mattr_reader :booking_url, default: nil
623
+
624
+ def self.booking_url=(url)
625
+ Booking.validate!(url)
626
+ @@booking_url = url.to_s.strip.empty? ? nil : Booking.page_url(url)
627
+ end
628
+
629
+ # Draw the booking page, GET /schedule (Studio::BookingsController, route
630
+ # helper studio_booking_path). OFF by default, like every route surface a
631
+ # consumer might already own: mcritchie-studio draws its own /schedule today,
632
+ # and its adoption turns this on as it deletes that route. With the flag on and
633
+ # no booking_url the page answers 404.
634
+ #
635
+ # config.draw_booking_routes = true
636
+ mattr_accessor :draw_booking_routes, default: false
637
+
638
+ # WHERE THE APP'S OWN BOOKING PAGE LIVES, for an app that renders
639
+ # `studio_booking_frame` in a view of its own instead of drawing the engine's
640
+ # /schedule: a path ("/schedule"), or a callable receiving the view
641
+ # (`->(view) { view.schedule_path }`). nil (the default) leaves the engine's
642
+ # page, when drawn, as the booking page. With it set, the page is treated as
643
+ # the engine's own is: a footer link to it opens the popup, `studio_booking_link`
644
+ # falls back to it, and it keeps the footer for a signed-in viewer. It wins
645
+ # over the engine's page when both are declared. It is written into hrefs, so
646
+ # it must be a local path (one leading "/"); see Studio::Booking.local_path?.
647
+ #
648
+ # config.booking_path = "/schedule"
649
+ mattr_reader :booking_path, default: nil
650
+
651
+ def self.booking_path=(declared)
652
+ @@booking_path = Booking.normalize_path(declared)
653
+ end
654
+
655
+ # The booking page's path for a view: Studio.booking_path, else the engine's
656
+ # /schedule when it is drawn, else nil.
657
+ def self.booking_path_for(view)
658
+ Booking.path_for(booking_path, view, drawn: draw_booking_routes)
659
+ end
660
+
661
+ # THE FRAME'S CROP AT REST: nil or false (the default) shows Google's whole
662
+ # page; a Hash of the schedule's measured box crops the frame to the slot
663
+ # picker until it is used. The numbers belong to ONE schedule, so there is no
664
+ # default crop: measure with `bin/booking-crop-measure <booking url>` and paste
665
+ # what it prints. Keys and derivation: lib/studio/booking.rb.
666
+ #
667
+ # config.booking_crop = { top: 200, bottom: 600, frame_height: 720 }
668
+ mattr_reader :booking_crop, default: nil
669
+
670
+ def self.booking_crop=(declared)
671
+ Booking.validate_crop!(declared)
672
+ Booking.crop(declared) # reports a crop that cannot be one, at boot
673
+ @@booking_crop = declared || nil
674
+ end
675
+
561
676
  # THE APP'S IDENTITY COPY, resolved: { title:, description:, image_url: }.
562
677
  # The operator's saved value (Studio::SiteIdentity, edited at
563
678
  # /admin/link_preview) wins, then the drafted Studio.site_title /
@@ -1057,6 +1172,17 @@ module Studio
1057
1172
  SidebarSections.resolve(sidebar_sections, view)
1058
1173
  end
1059
1174
 
1175
+ # The site footer's facts resolved for a view (lib/studio/site_footer.rb), or
1176
+ # nil when the app declared none. The name defaults to the site identity's
1177
+ # title and the logo to the navbar logo, so an app that has set those up says
1178
+ # them once.
1179
+ def self.site_footer_for(view)
1180
+ return nil if site_footer.nil?
1181
+
1182
+ SiteFooter.resolve(site_footer, view, name: site_identity[:title], logo: logo_for("Footer Logo"),
1183
+ booking_path: booking_path_for(view))
1184
+ end
1185
+
1060
1186
  # Navbar links resolved for a view context (lib/studio/navbar_links.rb).
1061
1187
  def self.navbar_links_for(view)
1062
1188
  NavbarLinks.resolve(navbar_links, view)
@@ -1261,6 +1387,12 @@ module Studio
1261
1387
  format: false, constraints: { username: %r{[^/]+} }
1262
1388
  end
1263
1389
 
1390
+ # The booking page (/schedule): Google Calendar's appointment page inline,
1391
+ # with the site footer. OPT-IN — see Studio.draw_booking_routes.
1392
+ if Studio.draw_booking_routes
1393
+ get "schedule", to: "studio/bookings#show", as: :studio_booking
1394
+ end
1395
+
1264
1396
  # The living style guide. Canonical at /admin/style (StyleController#index);
1265
1397
  # /admin/design_system redirects here but KEEPS its admin_design_system_path
1266
1398
  # helper so a shipped host sidebar link on the old helper still resolves.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.82.1
4
+ version: 0.84.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -309,10 +309,12 @@ files:
309
309
  - app/assets/images/state-flags/wy.svg
310
310
  - app/assets/javascripts/studio/alpine.js
311
311
  - app/assets/javascripts/studio/canvas_confetti.js
312
+ - app/assets/javascripts/studio/leaflet.js
312
313
  - app/assets/javascripts/studio/session.js
313
314
  - app/assets/javascripts/studio/sortable.js
314
315
  - app/assets/javascripts/studio/sticky_table_header.js
315
316
  - app/assets/javascripts/studio/studio_confetti.js
317
+ - app/assets/stylesheets/studio/leaflet.css
316
318
  - app/assets/stylesheets/studio/sticky_table_header.css
317
319
  - app/assets/tailwind/studio_engine/engine-motion.css
318
320
  - app/assets/tailwind/studio_engine/engine.css
@@ -334,6 +336,7 @@ files:
334
336
  - app/controllers/schema_controller.rb
335
337
  - app/controllers/sessions_controller.rb
336
338
  - app/controllers/solana_sessions_controller.rb
339
+ - app/controllers/studio/bookings_controller.rb
337
340
  - app/controllers/studio/email_images_controller.rb
338
341
  - app/controllers/studio/emails_controller.rb
339
342
  - app/controllers/studio/geo_settings_controller.rb
@@ -355,6 +358,7 @@ files:
355
358
  - app/helpers/studio/geo_helper.rb
356
359
  - app/helpers/studio/link_preview_helper.rb
357
360
  - app/helpers/studio/public_user_helper.rb
361
+ - app/helpers/studio/site_footer_helper.rb
358
362
  - app/helpers/studio_email_delivery_helper.rb
359
363
  - app/helpers/studio_navbar_helper.rb
360
364
  - app/helpers/studio_sidebar_helper.rb
@@ -445,6 +449,10 @@ files:
445
449
  - app/views/studio/board/_board.html.erb
446
450
  - app/views/studio/board/_card_shell.html.erb
447
451
  - app/views/studio/board/_column.html.erb
452
+ - app/views/studio/booking/_assets.html.erb
453
+ - app/views/studio/booking/_frame.html.erb
454
+ - app/views/studio/booking/_popup.html.erb
455
+ - app/views/studio/bookings/show.html.erb
448
456
  - app/views/studio/email_images/index.html.erb
449
457
  - app/views/studio/emails/_banner_editor.html.erb
450
458
  - app/views/studio/emails/_banner_preview.html.erb
@@ -529,10 +537,16 @@ files:
529
537
  - app/views/studio/profiles/show.html.erb
530
538
  - app/views/studio/public_users/not_found.html.erb
531
539
  - app/views/studio/public_users/show.html.erb
540
+ - app/views/studio/site_footer/_assets.html.erb
541
+ - app/views/studio/site_footer/_footer.html.erb
542
+ - app/views/studio/site_footer/_link.html.erb
543
+ - app/views/studio/site_footer/_map.html.erb
544
+ - app/views/studio/site_footer/_social_icon.html.erb
532
545
  - app/views/studio/site_identities/edit.html.erb
533
546
  - app/views/style/_host.html.erb
534
547
  - app/views/style/_modal_specimen.html.erb
535
548
  - app/views/style/_modals.html.erb
549
+ - app/views/style/_site_footer.html.erb
536
550
  - app/views/style/_specimen.html.erb
537
551
  - app/views/style/_tasks.html.erb
538
552
  - app/views/style/_theme.html.erb
@@ -573,6 +587,7 @@ files:
573
587
  - lib/generators/studio/site_identity/site_identity_generator.rb
574
588
  - lib/studio-engine.rb
575
589
  - lib/studio.rb
590
+ - lib/studio/booking.rb
576
591
  - lib/studio/cable.rb
577
592
  - lib/studio/color_scale.rb
578
593
  - lib/studio/email.rb
@@ -605,6 +620,7 @@ files:
605
620
  - lib/studio/session_fingerprint.rb
606
621
  - lib/studio/session_state.rb
607
622
  - lib/studio/sidebar_sections.rb
623
+ - lib/studio/site_footer.rb
608
624
  - lib/studio/theme_resolver.rb
609
625
  - lib/studio/ui_primitives.rb
610
626
  - lib/studio/username_generator.rb