DemoHostComponent.vala 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. using Spry;
  2. using Inversion;
  3. using Invercargill.DataStructures;
  4. /**
  5. * DemoHostComponent - A component that hosts demo implementations with source/demo toggle
  6. *
  7. * This component provides a frame for demo content with the ability to toggle
  8. * between viewing the demo and viewing the source code.
  9. *
  10. * Usage:
  11. * var host = factory.create<DemoHostComponent>();
  12. * host.demo_component_name = "SimpleCounterDemo";
  13. * host.source_file = "demo/DemoComponents/SimpleCounterDemo.vala";
  14. */
  15. public class DemoHostComponent : Component {
  16. private ComponentFactory factory = inject<ComponentFactory>();
  17. /// Name of the demo component to create and display
  18. public string demo_component_name { get; set; }
  19. /// Path to the source file to display (relative to project root)
  20. public string source_file { get; set; default = ""; }
  21. /// Toggle state - true when showing source code
  22. public bool showing_source { get; set; default = false; }
  23. /// The loaded source code content (cached after first load)
  24. private string? _source_content = null;
  25. public override string markup { get {
  26. return """
  27. <spry-context property="demo_component_name"/>
  28. <spry-context property="source_file"/>
  29. <div class="demo-host" sid="host" hx-swap="outerHTML">
  30. <div class="demo-header">
  31. <span class="demo-title" sid="title">Demo</span>
  32. <div class="demo-toggle-group">
  33. <button class="demo-toggle-btn" spry-action=":ShowDemo" spry-target="host"
  34. class-active-expr="!this.showing_source">Demo</button>
  35. <button class="demo-toggle-btn" spry-action=":ShowSource" spry-target="host"
  36. class-active-expr="this.showing_source">Source</button>
  37. </div>
  38. </div>
  39. <div class="demo-content" sid="content">
  40. <div spry-if="this.showing_source">
  41. <pre class="demo-source"><code sid="source-code"></code></pre>
  42. </div>
  43. <div spry-else>
  44. <spry-outlet sid="demo-outlet"/>
  45. </div>
  46. </div>
  47. </div>
  48. """;
  49. }}
  50. public override async void prepare() throws Error {
  51. // Update the title with the demo component name
  52. if (demo_component_name != null) {
  53. this["title"].text_content = demo_component_name;
  54. }
  55. // Create the demo component and add it to the outlet when showing demo
  56. if (!showing_source && source_file != null) {
  57. var demo = factory.create_by_name(demo_component_name);
  58. var series = new Series<Renderable>();
  59. series.add(demo);
  60. set_outlet_children("demo-outlet", series);
  61. }
  62. // Update the source code display if we're showing source
  63. if (showing_source && _source_content != null) {
  64. this["source-code"].text_content = _source_content;
  65. }
  66. }
  67. public async override void handle_action(string action) throws Error {
  68. switch (action) {
  69. case "ShowDemo":
  70. showing_source = false;
  71. break;
  72. case "ShowSource":
  73. if (_source_content == null) {
  74. _source_content = yield load_source_file();
  75. }
  76. showing_source = true;
  77. break;
  78. }
  79. }
  80. /**
  81. * Load the source file from disk and escape HTML entities
  82. */
  83. private async string load_source_file() {
  84. try {
  85. var path = source_file;
  86. // Try to read from the project root (relative path)
  87. var file = File.new_for_path(path);
  88. if (!file.query_exists()) {
  89. return @"Error: Source file not found: $path";
  90. }
  91. uint8[] contents;
  92. string etag_out;
  93. yield file.load_contents_async(null, out contents, out etag_out);
  94. var source = (string)contents;
  95. return escape_html(source);
  96. } catch (Error e) {
  97. return @"Error loading source file: $(e.message)";
  98. }
  99. }
  100. /**
  101. * Escape HTML entities for safe display in <pre><code> blocks
  102. */
  103. private string escape_html(string input) {
  104. var result = input
  105. .replace("&", "&amp;")
  106. .replace("<", "&lt;")
  107. .replace(">", "&gt;")
  108. .replace("\"", "&quot;")
  109. .replace("'", "&#39;");
  110. return result;
  111. }
  112. }