Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <div class="tnfr-view">
    <div class="top-row">
      <div class="top-row-left">
        <button type="button" :class="{ active: activeStore === 'tnfr' }" @click="handleStoreClick('tnfr')">tnfrStore</button>
        <button type="button" :class="{ active: activeStore === 'session' }" @click="handleStoreClick('session')">sessionStore</button>
        <button type="button" :class="{ active: activeStore === 'preferences' }" @click="handleStoreClick('preferences')">preferencesStore</button>
        <button type="button" :class="{ active: activeStore === 'currency' }" @click="handleStoreClick('currency')">currencyStore</button>
        <button type="button" :class="{ active: activeStore === 'organization' }" @click="handleStoreClick('organization')">organizationStore</button>
      </div>
      <div class="top-row-right">
        <button type="button" @click="handleClear">Clear</button>
      </div>
    </div>

    <!-- Jobs Dropdown -->
    <div class="jobs-dropdown-section">
      <label for="jobs-dropdown">Jobs (click to copy ID):</label>
      <select id="jobs-dropdown" @change="handleJobSelect($event)">
        <option value="">-- Select a job --</option>
        <option v-for="problem in sortedProblems" :key="problem.id" :value="problem.id">
          {{ problem.name }}
        </option>
      </select>
      <span v-if="copiedId" class="copied-message">Copied: {{ copiedId }}</span>
    </div>

    <!-- Function Caller for each store -->
    <StoreFunctionCaller
      v-if="activeStore === 'tnfr'"
      :function-list="tnfrFunctionDefs"
    />
    <StoreFunctionCaller
      v-else-if="activeStore === 'session'"
      :function-list="sessionFunctionDefs"
    />
    <StoreFunctionCaller
      v-else-if="activeStore === 'preferences'"
      :function-list="preferencesFunctionDefs"
    />
    <StoreFunctionCaller
      v-else-if="activeStore === 'currency'"
      :function-list="currencyFunctionDefs"
    />
    <StoreFunctionCaller
      v-else-if="activeStore === 'organization'"
      :function-list="organizationFunctionDefs"
    />
    <div v-if="jsonData" class="json-block">
      <div class="json-controls">
        <button type="button" @click="expandAllJson">Expand All</button>
        <button type="button" @click="collapseAllJson">Collapse All</button>
      </div>
      <pre><code><json-node :key="jsonKey" :data="jsonData" :indent="0" :start-collapsed="!jsonExpandAll" :expand-all="jsonExpandMode" /></code></pre>
    </div>
  </div>
</template>

<script lang="ts">
import { ref, computed, defineComponent, onMounted } from "vue";
import { useSessionStore } from "@/stores/session";
import { usePreferencesStore } from "@/stores/preferences";
import { useCurrencyStore } from "@/stores/currency";
import { useOrganizationStore } from "@/stores/organization";
import { useTnfrStore } from "@/stores/tnfr";
import StoreFunctionCaller, { type FunctionDefinition } from "@/components/StoreFunctionCaller.vue";
import JsonNode from "@/components/JsonViewer.vue";
import { loadDirectory, type DirectoryData } from "@/framework/directory";
import type { ProblemsRecord } from "@/pocketbase-types";

export default defineComponent({
  name: "TNFR",
  components: { JsonNode, StoreFunctionCaller },
  setup() {
    const sessionStore = useSessionStore();
    const preferencesStore = usePreferencesStore();
    const currencyStore = useCurrencyStore();
    const organizationStore = useOrganizationStore();
    const tnfrStore = useTnfrStore();

    const activeStore = ref<string | null>(null);
    const jsonExpandAll = ref(true);
    const jsonExpandMode = ref(false);
    const jsonKey = ref(0);
    const storeFunctions = ref<string[]>([]);

    // Jobs dropdown state
    const allProblems = ref<ProblemsRecord[]>([]);
    const copiedId = ref<string | null>(null);

    // Sorted problems alphabetically
    const sortedProblems = computed(() => {
      return [...allProblems.value].sort((a, b) =>
        (a.name || '').localeCompare(b.name || '')
      );
    });

    // Load problems on mount
    onMounted(async () => {
      try {
        const directoryData = await loadDirectory();
        allProblems.value = directoryData.problems;
      } catch (error) {
        console.error('[TNFR] Error loading directory:', error);
      }
    });

    // Handle job selection - copy ID to clipboard
    async function handleJobSelect(event: Event) {
      const select = event.target as HTMLSelectElement;
      const problemId = select.value;
      if (!problemId) return;

      try {
        await navigator.clipboard.writeText(problemId);
        copiedId.value = problemId;
        // Reset selection
        select.value = '';
        // Clear copied message after 2 seconds
        setTimeout(() => {
          copiedId.value = null;
        }, 2000);
      } catch (error) {
        console.error('[TNFR] Error copying to clipboard:', error);
      }
    }

    // Reactive computed property that auto-updates when store changes
    const jsonData = computed(() => {
      if (!activeStore.value) return null;

      switch (activeStore.value) {
        case 'tnfr':
          return {
            ...tnfrStore.$state,
            nextJob: tnfrStore.nextJob,
          };
        case 'session':
          return {
            jobs: sessionStore.jobs,
            appProgress: sessionStore.appProgress,
            customerProgress: sessionStore.customerProgress,
            secretMode: sessionStore.secretMode,
          };
        case 'preferences':
          return {
            darkMode: preferencesStore.darkMode,
            currency: preferencesStore.currency,
            role: preferencesStore.role,
            darkModeToggleCount: preferencesStore.darkModeToggleCount,
          };
        case 'currency':
          return {
            currencies: currencyStore.currencies,
            exchangeRates: currencyStore.exchangeRates,
          };
        case 'organization':
          return {
            hourlyFee: organizationStore.hourlyFee,
            saDiscount: organizationStore.saDiscount,
            serviceCallFee: organizationStore.serviceCallFee,
            salesTax: organizationStore.salesTax,
            isLoading: organizationStore.isLoading,
          };
        default:
          return null;
      }
    });

    function getStoreFunctions(store: any): string[] {
      return Object.keys(store).filter(key => typeof store[key] === 'function').sort();
    }

    function expandAllJson() {
      jsonExpandAll.value = true;
      jsonExpandMode.value = true;
      jsonKey.value++;
    }

    function collapseAllJson() {
      jsonExpandAll.value = false;
      jsonExpandMode.value = true;
      jsonKey.value++;
    }

    function handleClear() {
      activeStore.value = null;
      storeFunctions.value = [];
    }

    async function handleStoreClick(storeName: string) {
      handleClear();
      activeStore.value = storeName;

      switch (storeName) {
        case 'session':
          await sessionStore.load();
          storeFunctions.value = getStoreFunctions(sessionStore);
          break;
        case 'preferences':
          await preferencesStore.getPreferences();
          storeFunctions.value = getStoreFunctions(preferencesStore);
          break;
        case 'currency':
          await currencyStore.getCurrencies();
          await currencyStore.getRates();
          storeFunctions.value = getStoreFunctions(currencyStore);
          break;
        case 'organization':
          await organizationStore.loadVariables();
          storeFunctions.value = getStoreFunctions(organizationStore);
          break;
        case 'tnfr':
          await tnfrStore.load();
          await tnfrStore.getPreferences();
          await tnfrStore.getCurrencies();
          await tnfrStore.getRates();
          await tnfrStore.loadOrgVariables();
          storeFunctions.value = getStoreFunctions(tnfrStore);
          break;
      }
    }

    // Define session store function definitions
    const sessionFunctionDefs = computed<FunctionDefinition[]>(() => [
      {
        name: 'load',
        description: 'Load session state from database',
        handler: () => sessionStore.load(),
      },
      {
        name: 'save',
        description: 'Save current session state to database',
        handler: () => sessionStore.save(),
      },
      {
        name: 'clear',
        description: 'Clear all session data',
        handler: () => sessionStore.clear(),
      },
      {
        name: 'addJob',
        description: 'Add a new job to the session',
        params: [
          { name: 'title', type: 'string', required: true, placeholder: 'Job title' },
        ],
        handler: (title: string) => sessionStore.addJob(title),
      },
      {
        name: 'updateCustomerProgress',
        description: 'Update customer progress at index',
        params: [
          { name: 'stepIndex', type: 'number', required: true, placeholder: '0' },
          { name: 'value', type: 'string', required: true, placeholder: 'true or step name' },
        ],
        handler: (stepIndex: number, value: string | boolean) => sessionStore.updateCustomerProgress(stepIndex, value),
      },
    ]);

    // Define preferences store function definitions
    const preferencesFunctionDefs = computed<FunctionDefinition[]>(() => [
      {
        name: 'getPreferences',
        description: 'Load all preferences from storage',
        handler: () => preferencesStore.getPreferences(),
      },
      {
        name: 'getPreference',
        description: 'Get a single preference value',
        params: [
          { name: 'key', type: 'string', required: true, placeholder: 'e.g., darkMode, currency, role' },
        ],
        handler: (key: string) => preferencesStore.getPreference(key),
      },
      {
        name: 'setPreference',
        description: 'Set a preference value',
        params: [
          { name: 'key', type: 'string', required: true, placeholder: 'e.g., darkMode, currency, role' },
          { name: 'value', type: 'string', required: true, placeholder: 'Preference value' },
        ],
        handler: (key: string, value: string) => preferencesStore.setPreference(key, value),
      },
      {
        name: 'syncCurrencyPrefs',
        description: 'Sync currency preferences to userPrefs table',
        params: [
          { name: 'targetCurrency', type: 'string', required: true, placeholder: 'e.g., usd, gbp' },
        ],
        handler: (targetCurrency: string) => preferencesStore.syncCurrencyPrefs(targetCurrency),
      },
      {
        name: 'resetDarkModeToggleCount',
        description: 'Reset the dark mode toggle counter',
        handler: () => preferencesStore.resetDarkModeToggleCount(),
      },
    ]);

    // Define currency store function definitions
    const currencyFunctionDefs = computed<FunctionDefinition[]>(() => [
      {
        name: 'getCurrencies',
        description: 'Load available currencies from database',
        handler: () => currencyStore.getCurrencies(),
      },
      {
        name: 'getRates',
        description: 'Load exchange rates from database',
        handler: () => currencyStore.getRates(),
      },
      {
        name: 'syncCurrencyPrefs',
        description: 'Sync currency preferences to userPrefs table (one-way)',
        params: [
          { name: 'targetCurrency', type: 'string', required: true, placeholder: 'e.g., usd, gbp, eur' },
        ],
        handler: (targetCurrency: string) => currencyStore.syncCurrencyPrefs(targetCurrency),
      },
    ]);

    // Define organization store function definitions
    const organizationFunctionDefs = computed<FunctionDefinition[]>(() => [
      {
        name: 'loadVariables',
        description: 'Load organization variables (hourly fee, tax, etc.)',
        handler: () => organizationStore.loadVariables(),
      },
      {
        name: 'setHourlyFee',
        description: 'Set the hourly fee rate',
        params: [
          { name: 'fee', type: 'number', required: true, placeholder: '5.262' },
        ],
        handler: (fee: number) => { organizationStore.hourlyFee = fee; return { hourlyFee: fee }; },
      },
      {
        name: 'setSaDiscount',
        description: 'Set the service agreement discount',
        params: [
          { name: 'discount', type: 'number', required: true, placeholder: '1.5' },
        ],
        handler: (discount: number) => { organizationStore.saDiscount = discount; return { saDiscount: discount }; },
      },
      {
        name: 'setServiceCallFee',
        description: 'Set the service call fee',
        params: [
          { name: 'fee', type: 'number', required: true, placeholder: '2.0' },
        ],
        handler: (fee: number) => { organizationStore.serviceCallFee = fee; return { serviceCallFee: fee }; },
      },
      {
        name: 'setSalesTax',
        description: 'Set the sales tax rate',
        params: [
          { name: 'tax', type: 'number', required: true, placeholder: '1.08' },
        ],
        handler: (tax: number) => { organizationStore.salesTax = tax; return { salesTax: tax }; },
      },
    ]);

    // Define tnfr store function definitions with parameters
    const tnfrFunctionDefs = computed<FunctionDefinition[]>(() => [
      {
        name: 'load',
        description: 'Load session state from database',
        handler: () => tnfrStore.load(),
      },
      {
        name: 'save',
        description: 'Save current session state to database',
        handler: () => tnfrStore.save(),
      },
      {
        name: 'clear',
        description: 'Clear all session data',
        handler: () => tnfrStore.clear(),
      },
      {
        name: 'addJob',
        description: 'Add a new job to the cart by problem ID (loads job content automatically)',
        params: [
          { name: 'problemId', type: 'string', required: true, placeholder: 'Problem ID from jobs dropdown' },
        ],
        handler: (problemId: string) => tnfrStore.addJob(problemId),
      },
      {
        name: 'removeJob',
        description: 'Remove a job from the cart',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID to remove' },
        ],
        handler: (jobId: string) => tnfrStore.removeJob(jobId),
      },
      {
        name: 'setJobTitle',
        description: 'Set the title for a job',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'title', type: 'string', required: true, placeholder: 'New title' },
        ],
        handler: (jobId: string, title: string) => tnfrStore.setJobTitle(jobId, title),
      },
      {
        name: 'setJobEdited',
        description: 'Mark a job as edited',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'edited', type: 'boolean', required: true },
        ],
        handler: (jobId: string, edited: boolean) => tnfrStore.setJobEdited(jobId, edited),
      },
      {
        name: 'setHoursConfirmed',
        description: 'Mark hours as confirmed for a job',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'confirmed', type: 'boolean', required: true },
        ],
        handler: (jobId: string, confirmed: boolean) => tnfrStore.setHoursConfirmed(jobId, confirmed),
      },
      {
        name: 'updateExtraTime',
        description: 'Update extra time hours for a job',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'hours', type: 'number', required: true, placeholder: '0' },
        ],
        handler: (jobId: string, hours: number) => tnfrStore.updateExtraTime(jobId, hours),
      },
      {
        name: 'getPreferences',
        description: 'Load user preferences from storage',
        handler: () => tnfrStore.getPreferences(),
      },
      {
        name: 'setPreference',
        description: 'Set a user preference',
        params: [
          { name: 'key', type: 'string', required: true, placeholder: 'e.g., darkMode, currency, role' },
          { name: 'value', type: 'string', required: true, placeholder: 'Preference value' },
        ],
        handler: (key: string, value: string) => tnfrStore.setPreference(key, value),
      },
      {
        name: 'getCurrencies',
        description: 'Load available currencies from database',
        handler: () => tnfrStore.getCurrencies(),
      },
      {
        name: 'getRates',
        description: 'Load exchange rates from database',
        handler: () => tnfrStore.getRates(),
      },
      {
        name: 'loadOrgVariables',
        description: 'Load organization variables (hourly fee, tax, etc.)',
        handler: () => tnfrStore.loadOrgVariables(),
      },
      {
        name: 'loadDefaultJob1',
        description: 'Load PA1 "Fixture drain cleaning interior" into defaultJob1',
        handler: () => tnfrStore.loadDefaultJob1(),
      },
      {
        name: 'createInvoice',
        description: 'Create and return invoice data with line items, totals, and payment plan info',
        handler: () => tnfrStore.createInvoice(),
      },
      {
        name: 'addProblemToJob',
        description: 'Add a problem to an existing job (requires ProblemsRecord object - use programmatically)',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
        ],
        handler: () => 'This function requires a ProblemsRecord object. Use programmatically via tnfrStore.addProblemToJob(jobId, problemRecord)',
      },
      {
        name: 'setSelectedOffer',
        description: 'Set the selected offer for a job (also updates invoice)',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'offerId', type: 'string', required: false, placeholder: 'Offer ID (null to clear)' },
          { name: 'price', type: 'string', required: false, placeholder: 'Price' },
          { name: 'tierName', type: 'string', required: false, placeholder: 'Tier name' },
          { name: 'tierTitle', type: 'string', required: false, placeholder: 'Tier title' },
        ],
        handler: (jobId: string, offerId: string, price: string, tierName: string, tierTitle: string) =>
          tnfrStore.setSelectedOffer(jobId, offerId ? { id: offerId } as any : null, price || null, tierName || null, tierTitle || null),
      },
      {
        name: 'setJobContent',
        description: 'Set the content/menus for a job',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'content', type: 'string', required: true, placeholder: 'JSON content object' },
        ],
        handler: (jobId: string, content: string) => {
          try {
            const parsed = JSON.parse(content);
            return tnfrStore.setJobContent(jobId, parsed);
          } catch (e) {
            console.error('Invalid JSON:', e);
            return Promise.reject('Invalid JSON content');
          }
        },
      },
      {
        name: 'calculateMonthlyPayment',
        description: 'Calculate monthly payment using amortization formula',
        params: [
          { name: 'principal', type: 'number', required: true, placeholder: 'Loan amount (principal)' },
        ],
        handler: (principal: number) => tnfrStore.calculateMonthlyPayment(principal),
      },
      {
        name: 'getMenuBullets',
        description: 'Get all unique bullet points from all tiers in a job\'s menu',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
        ],
        handler: (jobId: string) => tnfrStore.getMenuBullets(jobId),
      },
      {
        name: 'getTierBullets',
        description: 'Get bullet points for a specific tier',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'tierId', type: 'string', required: true, placeholder: 'Tier ID' },
        ],
        handler: (jobId: string, tierId: string) => tnfrStore.getTierBullets(jobId, tierId),
      },
      {
        name: 'addMenuBullet',
        description: 'Add a new bullet to the menu pool (adds to all tiers)',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'content', type: 'string', required: true, placeholder: 'Bullet content' },
        ],
        handler: (jobId: string, content: string) => tnfrStore.addMenuBullet(jobId, content),
      },
      {
        name: 'editMenuBullet',
        description: 'Edit a bullet\'s content (updates in all tiers)',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'bulletId', type: 'string', required: true, placeholder: 'Bullet ID' },
          { name: 'newContent', type: 'string', required: true, placeholder: 'New content' },
        ],
        handler: (jobId: string, bulletId: string, newContent: string) => tnfrStore.editMenuBullet(jobId, bulletId, newContent),
      },
      {
        name: 'removeMenuBullet',
        description: 'Remove a bullet from menu and all tiers',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'bulletId', type: 'string', required: true, placeholder: 'Bullet ID' },
        ],
        handler: (jobId: string, bulletId: string) => tnfrStore.removeMenuBullet(jobId, bulletId),
      },
      {
        name: 'assignMenuBulletToTier',
        description: 'Assign a bullet from menu pool to a specific tier',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'tierId', type: 'string', required: true, placeholder: 'Tier ID' },
          { name: 'bulletId', type: 'string', required: true, placeholder: 'Bullet ID' },
        ],
        handler: (jobId: string, tierId: string, bulletId: string) => tnfrStore.assignMenuBulletToTier(jobId, tierId, bulletId),
      },
      {
        name: 'removeBulletFromTier',
        description: 'Remove a bullet from a specific tier only (keeps in menu pool)',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'tierId', type: 'string', required: true, placeholder: 'Tier ID' },
          { name: 'bulletId', type: 'string', required: true, placeholder: 'Bullet ID' },
        ],
        handler: (jobId: string, tierId: string, bulletId: string) => tnfrStore.removeBulletFromTier(jobId, tierId, bulletId),
      },
      {
        name: 'addNote',
        description: 'Add a note to a job',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'note', type: 'string', required: true, placeholder: 'Note text' },
        ],
        handler: (jobId: string, note: string) => tnfrStore.addNote(jobId, note),
      },
      {
        name: 'removeNote',
        description: 'Remove a note from a job',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
          { name: 'note', type: 'string', required: true, placeholder: 'Note text to remove' },
        ],
        handler: (jobId: string, note: string) => tnfrStore.removeNote(jobId, note),
      },
      {
        name: 'markMenuSeen',
        description: 'Mark a menu as seen for a job',
        params: [
          { name: 'jobId', type: 'string', required: true, placeholder: 'Job ID' },
        ],
        handler: (jobId: string) => tnfrStore.markMenuSeen(jobId),
      },
      {
        name: 'syncCurrencyPrefs',
        description: 'Sync currency preferences to userPrefs table (one-way)',
        params: [
          { name: 'targetCurrency', type: 'string', required: true, placeholder: 'e.g., usd, gbp, eur' },
        ],
        handler: (targetCurrency: string) => tnfrStore.syncCurrencyPrefs(targetCurrency),
      },
    ]);

    return {
      jsonData,
      activeStore,
      jsonExpandAll,
      jsonExpandMode,
      jsonKey,
      storeFunctions,
      tnfrFunctionDefs,
      sessionFunctionDefs,
      preferencesFunctionDefs,
      currencyFunctionDefs,
      organizationFunctionDefs,
      sortedProblems,
      copiedId,
      expandAllJson,
      collapseAllJson,
      handleClear,
      handleStoreClick,
      handleJobSelect,
    };
  },
});
</script>

<style scoped>
.tnfr-view {
  padding: 16px;
  padding-bottom: 100px;
  font-family: system-ui, sans-serif;
  min-height: 100vh;
  overflow-y: auto;
}

.top-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.top-row-left,
.top-row-right {
  display: flex;
  gap: 8px;
}

.jobs-dropdown-section {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
  padding: 12px;
  background: var(--ion-background-color-step-50, #f5f5f5);
  border: 1px solid var(--ion-color-step-200, #ddd);
  border-radius: 4px;
}

.jobs-dropdown-section label {
  font-size: 14px;
  font-weight: 500;
  color: var(--ion-text-color);
}

.jobs-dropdown-section select {
  flex: 1;
  max-width: 400px;
  padding: 8px 12px;
  font-size: 14px;
  border: 1px solid var(--ion-color-step-300, #ccc);
  border-radius: 4px;
  background: var(--ion-background-color, #fff);
  color: var(--ion-text-color);
  cursor: pointer;
}

.copied-message {
  font-size: 12px;
  color: var(--ion-color-success);
  font-weight: 500;
}

button {
  padding: 8px 16px;
  font-size: 14px;
  cursor: pointer;
  background: var(--ion-background-color-step-100, #f0f0f0);
  border: 1px solid var(--ion-color-step-300, #ccc);
  color: var(--ion-text-color, #000);
  border-radius: 4px;
}

button:hover {
  background: var(--ion-background-color-step-150, #e0e0e0);
}

button.active {
  background: var(--ion-color-primary);
  color: var(--ion-color-primary-contrast);
}

.functions-block {
  margin-top: 16px;
  padding: 12px;
  background: var(--ion-background-color-step-50, #f5f5f5);
  border: 1px solid var(--ion-color-step-200, #ddd);
  border-radius: 4px;
}

.functions-block h3 {
  margin: 0 0 8px 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--ion-text-color);
}

.functions-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.functions-list code {
  padding: 4px 8px;
  background: var(--ion-background-color-step-100, #e8e8e8);
  border-radius: 4px;
  font-size: 12px;
  color: var(--ion-color-primary);
  border-color: var(--ion-color-primary-shade);
}

.json-block {
  margin-top: 16px;
}

.json-block pre {
  margin-top: 0;
}

.json-controls {
  display: flex;
  gap: 8px;
  margin-bottom: 8px;
}

.json-controls button {
  padding: 4px 10px;
  font-size: 12px;
}

pre {
  margin-top: 16px;
  padding: 12px;
  background: var(--ion-background-color-step-50, #f5f5f5);
  border: 1px solid var(--ion-color-step-200, #ddd);
  border-radius: 4px;
  overflow-x: auto;
}

code {
  font-family: monospace;
  font-size: 12px;
  white-space: pre;
  color: var(--ion-text-color);
}
</style>