{"id":36969067,"url":"https://github.com/zeixcom/le-truc","last_synced_at":"2026-02-17T10:30:43.827Z","repository":{"id":328891713,"uuid":"1096444603","full_name":"zeixcom/le-truc","owner":"zeixcom","description":"Le Truc - the thing for type-safe reactive web components","archived":false,"fork":false,"pushed_at":"2025-12-18T08:29:59.000Z","size":1132,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-19T13:15:37.853Z","etag":null,"topics":["effects","javascript","reactivity","signals","typescript","web-components"],"latest_commit_sha":null,"homepage":"https://zeixcom.github.io/le-truc/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zeixcom.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-14T12:42:15.000Z","updated_at":"2025-12-18T02:50:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/zeixcom/le-truc","commit_stats":null,"previous_names":["zeixcom/le-truc"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/zeixcom/le-truc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeixcom%2Fle-truc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeixcom%2Fle-truc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeixcom%2Fle-truc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeixcom%2Fle-truc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zeixcom","download_url":"https://codeload.github.com/zeixcom/le-truc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeixcom%2Fle-truc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28399259,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["effects","javascript","reactivity","signals","typescript","web-components"],"created_at":"2026-01-13T21:02:18.504Z","updated_at":"2026-02-17T10:30:43.819Z","avatar_url":"https://github.com/zeixcom.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Le Truc\n\nVersion 0.16.0\n\n**Le Truc - the thing for type-safe reactive Web Components**\n\nLe Truc helps you create reusable, interactive Web Components that work with any backend or static site generator. Build once, use everywhere.\n\nLe Truc is a set of functions to build reusable, loosely coupled Web Components with reactive properties. It provides structure through components and simplifies state management and DOM synchronization using signals and effects, leading to more organized and maintainable code without a steep learning curve.\n\nUnlike SPA frameworks (React, Vue, Svelte, etc.) Le Truc takes a HTML-first approach, progressively enhancing server-rendered HTML rather than recreating (rendering) it using JavaScript. Le Truc achieves the same result as SPA frameworks with SSR, with a simpler, more efficient approach.\n\n## Quick Start\n\nAdd interactivity to your HTML in three steps:\n\n1. Start with HTML:\n\n```html\n\u003cbasic-hello\u003e\n  \u003clabel for=\"name\"\u003eYour name\u003c/label\u003e\n  \u003cinput id=\"name\" name=\"name\" type=\"text\" autocomplete=\"given-name\" /\u003e\n  \u003cp\u003eHello, \u003coutput for=\"name\"\u003eWorld\u003c/output\u003e!\u003c/p\u003e\n\u003c/basic-hello\u003e\n```\n\n2. Define the component:\n\n```js\nimport { asString, defineComponent, on, setText } from '@zeix/le-truc'\n\ndefineComponent(\n  'basic-hello',               // 1. Component name\n  { name: asString('World') }, // 2. Reactive property\n  q =\u003e ({                      // 3. Find DOM elements\n    input: q.first('input'),\n    output: q.first('output'),\n  }),\n  ({ host, input }) =\u003e ({      // 4. Define behavior\n    input: on('input', () =\u003e { host.name = input.value }),\n    output: setText('name'),\n  }),\n)\n```\n\n3. Import and watch it work!\n\n## Key Features\n\n- 🧱 **HTML Web Components**: Build on standard HTML and enhance it with reusable Web Components. No Virtual DOM – Le Truc works directly with the real DOM.\n- 🚦 **Reactive Properties**: Get and set values like with normal element properties, but they automatically track reads and notify on changes (signals).\n- ⚡️ **Fine-grained Effects**: Pinpoint updates to the parts of the DOM that need updating, avoiding unnecessary re-renders.\n- 🧩 **Function Composition**: Declare component behavior by composing small, reusable functions (parsers and effects).\n- 🛠️ **Customizable**: Le Truc is designed to be easily customizable and extensible. Create your own custom parsers and effects to suit your specific needs.\n- 🌐 **Context Support**: Share global states across components without prop drilling or tightly coupling logic.\n- 🪶 **Tiny footprint**: Minimal core (~10kB gzipped) with tree-shaking support, minimizing JavaScript bundle size.\n- 🛡️ **Type Safety**: Early warnings when types don't match improve code quality and reduce bugs.\n\nLe Truc uses [Cause \u0026 Effect](https://github.com/zeixcom/cause-effect) internally for state management with signals and glitch-free DOM updates. If wanted, you could fork Le Truc and replace Cause \u0026 Effect with a different state management library without changes to the user-facing `defineComponent()` API.\n\n## Installation\n\n```bash\n# with npm\nnpm install @zeix/le-truc\n\n# or with bun\nbun add @zeix/le-truc\n```\n\n## Documentation\n\nThe full documentation is still work in progress. The following chapters are already reasonably complete:\n\n- [Introduction](https://zeixcom.github.io/le-truc/index.html)\n- [Getting Started](https://zeixcom.github.io/le-truc/getting-started.html)\n- [Components](https://zeixcom.github.io/le-truc/components.html)\n- [Styling](https://zeixcom.github.io/le-truc/styling.html)\n- [Data Flow](https://zeixcom.github.io/le-truc/data-flow.html)\n- [About](https://zeixcom.github.io/le-truc/about.html)\n\n## Basic Usage\n\n1. Start with HTML:\n\n```html\n\u003cbasic-counter\u003e\n  \u003cbutton type=\"button\"\u003e💐 \u003cspan\u003e5\u003c/span\u003e\u003c/button\u003e\n\u003c/basic-counter\u003e\n```\n\n2. Define the component:\n\n```js\nimport { asInteger, defineComponent, on, read, setText } from '@zeix/le-truc'\n\nexport default defineComponent(\n  // 1. Component name\n  'basic-counter',\n\n  // 2. Reactive properties (signals)\n  {\n    // Count property is read from the DOM (ui.count) and converted to an integer\n    count: read(ui =\u003e ui.count.textContent, asInteger()),\n  },\n\n  // 3. Find DOM elements\n  ({ first }) =\u003e ({\n    // first() returns the first element matching the selector\n    increment: first(\n      'button',\n      'Add a native button element to increment the count.',\n    ),\n    count: first('span', 'Add a span to display the count.'),\n  }),\n\n  // 4. Define behavior (effects)\n  ({ host }) =\u003e ({ // host is the component element with reactive properties\n    // Add a click event listener to the increment button\n    increment: on('click', () =\u003e {\n      host.count++\n    }),\n    // Set the text of the count element to the count property whenever it changes\n    count: setText('count'),\n  }),\n)\n```\n\nExample styles:\n\n```css\nbasic-counter {\n  \u0026 button {\n    border: 1px solid var(--color-border);\n    border-radius: var(--space-xs);\n    background-color: var(--color-secondary);\n    padding: var(--space-xs) var(--space-s);\n    cursor: pointer;\n    color: var(--color-text);\n    font-size: var(--font-size-m);\n    line-height: var(--line-height-xs);\n    transition: background-color var(--transition-short) var(--easing-inout);\n\n    \u0026:hover {\n      background-color: var(--color-secondary-hover);\n    }\n\n    \u0026:active {\n      background-color: var(--color-secondary-active);\n    }\n  }\n}\n```\n\n3. Import and watch it work!\n\n## Advanced Examples\n\n### Tab Group\n\nAn example demonstrating how to create a fully accessible tab navigation.\n\nServer-rendered markup:\n\n```html\n\u003cmodule-tabgroup\u003e\n  \u003cdiv role=\"tablist\"\u003e\n    \u003cbutton\n      type=\"button\"\n      role=\"tab\"\n      id=\"trigger1\"\n      aria-controls=\"panel1\"\n      aria-selected=\"true\"\n      tabindex=\"0\"\n    \u003e\n      Tab 1\n    \u003c/button\u003e\n    \u003cbutton\n      type=\"button\"\n      role=\"tab\"\n      id=\"trigger2\"\n      aria-controls=\"panel2\"\n      aria-selected=\"false\"\n      tabindex=\"-1\"\n    \u003e\n      Tab 2\n    \u003c/button\u003e\n    \u003cbutton\n      type=\"button\"\n      role=\"tab\"\n      id=\"trigger3\"\n      aria-controls=\"panel3\"\n      aria-selected=\"false\"\n      tabindex=\"-1\"\n    \u003e\n      Tab 3\n    \u003c/button\u003e\n  \u003c/div\u003e\n  \u003cdiv role=\"tabpanel\" id=\"panel1\" aria-labelledby=\"trigger1\"\u003e\n    Tab 1 content\n  \u003c/div\u003e\n  \u003cdiv role=\"tabpanel\" id=\"panel2\" aria-labelledby=\"trigger2\" hidden\u003e\n    Tab 2 content\n  \u003c/div\u003e\n  \u003cdiv role=\"tabpanel\" id=\"panel3\" aria-labelledby=\"trigger3\" hidden\u003e\n    Tab 3 content\n  \u003c/div\u003e\n\u003c/module-tabgroup\u003e\n```\n\nLe Truc component:\n\n```js\nimport { createEventsSensor, defineComponent, read, setProperty, show } from '@zeix/le-truc'\n\nconst getAriaControls = element =\u003e element.getAttribute('aria-controls') ?? ''\n\nconst getSelected = (tabs, isCurrent, offset = 0) =\u003e {\n  const currentIndex = tabs.findIndex(isCurrent)\n  const newIndex = (currentIndex + offset + tabs.length) % tabs.length\n  return getAriaControls(tabs[newIndex])\n}\n\nexport default defineComponent(\n  // 1. Component name\n  'module-tabgroup',\n\n  // 2. Reactive properties (signals)\n  {\n    // Sensors are read-only signals that update on user interaction only (events)\n    selected: createEventsSensor(\n      // Initial value from aria-selected attribute\n      read(ui =\u003e getSelected(ui.tabs.get(), tab =\u003e tab.ariaSelected === 'true'), ''),\n      // Target element(s) key\n      'tabs',\n      // Event handlers return a value to update the signal\n      {\n        click: ({ target }) =\u003e getAriaControls(target),\n        keyup: ({ event, ui, target }) =\u003e {\n          const key = event.key\n          if (\n            [\n              'ArrowLeft',\n              'ArrowRight',\n              'ArrowUp',\n              'ArrowDown',\n              'Home',\n              'End',\n            ].includes(key)\n          ) {\n            event.preventDefault()\n            event.stopPropagation()\n            const tabs = ui.tabs.get()\n            const next =\n              key === 'Home'\n                ? getAriaControls(tabs[0])\n                : key === 'End'\n                  ? getAriaControls(tabs[tabs.length - 1])\n                  : getSelected(\n                      tabs,\n                      tab =\u003e tab === target,\n                      key === 'ArrowLeft' || key === 'ArrowUp' ? -1 : 1,\n                    )\n            tabs.filter(tab =\u003e getAriaControls(tab) === next)[0].focus()\n            return next\n          }\n        },\n      },\n    ),\n  },\n\n  // 3. Find DOM elements\n  ({ all }) =\u003e ({\n    // all() returns a Memo\u003cE[]\u003e that holds all elements matching the selector,\n    // dynamically updating when the DOM changes via MutationObserver\n    tabs: all(\n      'button[role=\"tab\"]',\n      'At least 2 tabs as children of a \u003c[role=\"tablist\"]\u003e element are needed. Each tab must reference a unique id of a \u003c[role=\"tabpanel\"]\u003e element.',\n    ),\n    panels: all(\n      '[role=\"tabpanel\"]',\n      'At least 2 tabpanels are needed. Each tabpanel must have a unique id.',\n    ),\n  }),\n\n  // 4. Define behavior (effects)\n  ({ host }) =\u003e {\n    // Extracted function to check if a tab is the current selected tab\n    const isCurrentTab = tab =\u003e host.selected === getAriaControls(tab)\n\n    return {\n      // Set properties on tabs based on their selection status\n      tabs: [\n        setProperty('ariaSelected', target =\u003e String(isCurrentTab(target))),\n        setProperty('tabIndex', target =\u003e (isCurrentTab(target) ? 0 : -1)),\n      ],\n      // Toggle visibility of panels based on the selected tab\n      panels: show(target =\u003e host.selected === target.id),\n    }\n  },\n)\n```\n\nExample styles:\n\n```css\nmodule-tabgroup {\n  display: block;\n  margin-bottom: var(--space-l);\n\n  \u003e [role=\"tablist\"] {\n    display: flex;\n    border-bottom: 1px solid var(--color-border);\n    padding: 0;\n    margin-bottom: 0;\n\n    \u003e [role=\"tab\"] {\n      border: 0;\n      border-top: 2px solid transparent;\n      border-bottom-width: 0;\n      border-radius: var(--space-xs) var(--space-xs) 0 0;\n      font-family: var(--font-family-sans);\n      font-size: var(--font-size-s);\n      font-weight: var(--font-weight-bold);\n      padding: var(--space-s) var(--space-m);\n      color: var(--color-text-soft);\n      background-color: var(--color-secondary);\n      cursor: pointer;\n      transition: all var(--transition-short) var(--easing-inout);\n\n      \u0026:hover,\n      \u0026:focus {\n        color: var(--color-text);\n        background-color: var(--color-secondary-hover);\n      }\n\n      \u0026:focus {\n        z-index: 1;\n      }\n\n      \u0026:active {\n        color: var(--color-text);\n        background-color: var(--color-secondary-active);\n      }\n\n      \u0026[aria-selected=\"true\"] {\n        color: var(--color-primary-active);\n        border-top: 3px solid var(--color-primary);\n        background-color: var(--color-background);\n        margin-bottom: -1px;\n      }\n    }\n  }\n\n  \u003e [role=\"tabpanel\"] {\n    font-family: sans-serif;\n    font-size: var(--font-size-m);\n    background: var(--color-background);\n    margin-block: var(--space-l);\n  }\n}\n```\n\n### Lazy Load\n\nAn example demonstrating how to use a custom attribute parser (sanitize an URL) and a signal producer (async fetch) to implement lazy loading.\n\n```html\n\u003cmodule-lazyload src=\"/module-lazyload/snippet.html\"\u003e\n  \u003ccard-callout\u003e\n    \u003cp class=\"loading\" role=\"status\"\u003eLoading...\u003c/p\u003e\n    \u003cp class=\"error\" role=\"alert\" aria-live=\"assertive\" hidden\u003e\u003c/p\u003e\n  \u003c/card-callout\u003e\n  \u003cdiv class=\"content\" hidden\u003e\u003c/div\u003e\n\u003c/module-lazyload\u003e\n```\n\nLe Truc component:\n\n```js\nimport {\n  asString,\n  type Component,\n  createTask,\n  dangerouslySetInnerHTML,\n  defineComponent,\n  setText,\n  show,\n  toggleClass,\n} from '@zeix/le-truc'\n\nexport default defineComponent(\n  // 1. Component name\n  'module-lazyload',\n\n  // 2. Reactive properties (signals)\n  {\n    src: asString(),\n  },\n\n  // 3. Find DOM elements\n  ({ first }) =\u003e ({\n    callout: first(\n      'card-callout',\n      'Needed to display loading state and error messages.',\n    ),\n    loading: first('.loading', 'Needed to display loading state.'),\n    error: first('.error', 'Needed to display error messages.'),\n    content: first('.content', 'Needed to display content.'),\n  }),\n\n  // 4. Define behavior (effects)\n  ui =\u003e {\n    const { host } = ui\n\n    // Private async task signal to fetch content from the provided URL\n    const result = createTask(\n      async (_prev, abort) =\u003e {\n        const url = host.src\n        const error = !url\n          ? 'No URL provided'\n          : !isValidURL(url)\n            ? 'Invalid URL'\n            : isRecursiveURL(url, host)\n              ? 'Recursive URL detected'\n              : ''\n        if (error) return { ok: false, value: '', error, pending: false }\n\n        try {\n          const response = await fetch(url, abort)\n          if (!response.ok) throw new Error(`HTTP error: ${response.statusText}`)\n          const content = await response.text()\n          return { ok: true, value: content, error: '', pending: false }\n        } catch (error) {\n          return {\n            ok: false,\n            value: '',\n            error: `Failed to fetch content for \"${url}\": ${String(error)}`,\n            pending: false,\n          }\n        }\n      },\n      // Initial value of the signal before the Promise is resolved\n      { value: { ok: false, value: '', error: '', pending: true } },\n    )\n\n    // Extracted function to check if an error occurred\n    const hasError = () =\u003e !!result.get().error\n\n    return {\n      callout: [show(() =\u003e !result.get().ok), toggleClass('danger', hasError)],\n      loading: show(() =\u003e !!result.get().pending),\n      error: [show(hasError), setText(() =\u003e result.get().error ?? '')],\n      content: [\n        show(() =\u003e result.get().ok),\n        // Set inner HTML to the fetched content (use only for trusted sources)\n        dangerouslySetInnerHTML(() =\u003e result.get().value ?? '', {\n          allowScripts: host.hasAttribute('allow-scripts'),\n        }),\n      ],\n    }\n  },\n)\n```\n\n## Testing\n\nLe Truc components come with comprehensive Playwright tests to ensure reliability and compatibility across browsers.\n\n### Running All Tests\n\n```bash\n# Run all component tests\nbun run test\n\n# Run all tests with specific options\nbunx playwright test examples --headed --reporter=html\n```\n\n### Running Individual Component Tests\n\nFor faster development and debugging, you can run tests for specific components:\n\n```bash\n# Run tests for a single component\nbun run test:component module-carousel\nbun run test:component basic-hello\nbun run test:component form-combobox\n\n# Run with Playwright options\nbun run test:component module-carousel --headed --debug\nbun run test:component basic-hello -- --reporter=html\n\n# See all available components\nbun run test:component --help\n```\n\n### Test Structure\n\nEach component has its own test file following the pattern:\n- `examples/[component-name]/[component-name].spec.ts`\n- Tests cover functionality, accessibility, and edge cases\n- Tests run against actual component implementations in browsers\n\n### Development Server for Testing\n\nThe test runner uses a specialized server that:\n- Builds examples automatically before testing\n- Disables HMR for test stability (via `PLAYWRIGHT=1`)\n- Serves component test pages at `/test/[component-name]`\n\n## Contributing \u0026 License\n\nFeel free to contribute, report issues, or suggest improvements.\n\nLicense: [MIT](LICENSE)\n\n(c) 2026 [Zeix AG](https://zeix.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzeixcom%2Fle-truc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzeixcom%2Fle-truc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzeixcom%2Fle-truc/lists"}