@invisibleloop/pulse 0.20.10 → 0.20.19
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.
- package/package.json +6 -5
- package/public/.pulse-ui-version +1 -1
- package/scripts/build.js +26 -0
- package/scripts/release-version.js +17 -8
- package/scripts/strip-server.test.js +60 -0
- package/src/agent/checklist.md +8 -12
- package/src/agent/commands/pulse-start.md +2 -0
- package/src/agent/commands/verify.md +30 -8
- package/src/agent/guide-components.md +7 -25
- package/src/agent/guide-routing.md +0 -12
- package/src/agent/guide-server.md +102 -1
- package/src/agent/guide-spec.md +8 -1
- package/src/agent/guide-styles.md +23 -0
- package/src/agent/identity.md +9 -0
- package/src/agent/workflow.md +4 -23
- package/src/cli/check-bundles.js +113 -0
- package/src/cli/check-bundles.test.js +137 -0
- package/src/cli/cli.test.js +105 -0
- package/src/cli/dev.js +48 -3
- package/src/cli/diagnose.js +105 -0
- package/src/cli/diagnose.test.js +124 -0
- package/src/cli/discover.js +9 -2
- package/src/cli/index.js +171 -9
- package/src/cli/process-utils.js +57 -0
- package/src/cli/process-utils.test.js +114 -0
- package/src/cli/review.js +391 -0
- package/src/cli/review.test.js +143 -0
- package/src/cli/scaffold.js +9 -4
- package/src/cli/stamp.js +102 -0
- package/src/cli/stamp.test.js +149 -0
- package/src/cli/start.js +15 -2
- package/src/cli/validate.js +184 -0
- package/src/cli/validate.test.js +153 -0
- package/src/mcp/mcp.test.js +261 -30
- package/src/mcp/server.js +207 -515
- package/src/mcp/validate-worker.js +18 -1
- package/src/runtime/index.js +144 -11
- package/src/runtime/runtime.test.js +199 -0
- package/src/runtime/ssr.js +14 -7
- package/src/runtime/ssr.test.js +31 -0
- package/src/server/index.js +163 -10
- package/src/server/server.test.js +225 -2
- package/src/spec/schema.js +1 -1
- package/src/ui/index.js +0 -5
- package/src/ui/ui.test.js +0 -75
- package/types/ui.d.ts +0 -21
- package/.claude/commands/build-page.md +0 -63
- package/.claude/commands/new-doc-page.md +0 -45
- package/.claude/commands/pulse-start.md +0 -11
- package/.claude/commands/pulse-stop.md +0 -5
- package/.claude/commands/verify.md +0 -118
- package/.claude/pulse-checklist.md +0 -189
- package/.github/workflows/ci.yml +0 -25
- package/.github/workflows/publish.yml +0 -45
- package/.pulse/load-reports/home/1773432711417.json +0 -22
- package/.pulse-verified +0 -1
- package/AGENTS.md +0 -697
- package/CLAUDE.md +0 -697
- package/README.md +0 -95
- package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
- package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
- package/benchmark/public/dist/manifest.json +0 -5
- package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
- package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
- package/benchmark/results/2026-03-27-09-20.json +0 -188
- package/benchmark/results/2026-03-27-09-21.json +0 -188
- package/benchmark/results/2026-03-27-09-29.json +0 -188
- package/benchmark/results/2026-03-27-10-14.json +0 -188
- package/benchmark/results/2026-03-27-10-18.json +0 -232
- package/benchmark/src/pages/counter.js +0 -37
- package/benchmark/src/pages/home.js +0 -21
- package/docs/.claude/commands/pulse-build.md +0 -7
- package/docs/.claude/commands/pulse-dev.md +0 -7
- package/docs/.claude/commands/pulse-start.md +0 -9
- package/docs/.claude/commands/pulse-stop.md +0 -5
- package/docs/.claude/commands/verify.md +0 -84
- package/docs/.claude/coverage-check.js +0 -196
- package/docs/.claude/pulse-checklist.md +0 -189
- package/docs/public/.pulse-ui-version +0 -1
- package/docs/public/andypixel.css +0 -237
- package/docs/public/bookworms.css +0 -422
- package/docs/public/coutumes.css +0 -561
- package/docs/public/docs.css +0 -2388
- package/docs/public/favicon.svg +0 -4
- package/docs/public/home-brut.css +0 -982
- package/docs/public/images/bookworms/hero-books.jpg +0 -0
- package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/docs/public/images/journey/178.jpg +0 -0
- package/docs/public/images/journey/29.jpg +0 -0
- package/docs/public/images/retrowheels/hero.jpg +0 -0
- package/docs/public/images/shop/devbook-pro.jpg +0 -0
- package/docs/public/images/suzukibento/1015.jpg +0 -0
- package/docs/public/images/suzukibento/1025.jpg +0 -0
- package/docs/public/images/suzukibento/1043.jpg +0 -0
- package/docs/public/images/suzukibento/1059.jpg +0 -0
- package/docs/public/images/suzukibento/1062.jpg +0 -0
- package/docs/public/images/suzukibento/1084.jpg +0 -0
- package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/docs/public/img/heic1501a.jpg +0 -0
- package/docs/public/img/placeholder-landscape.svg +0 -18
- package/docs/public/img/placeholder-square.svg +0 -14
- package/docs/public/img/placeholder-tall.svg +0 -22
- package/docs/public/journey.css +0 -528
- package/docs/public/menu.js +0 -327
- package/docs/public/pulse-ui.css +0 -3028
- package/docs/public/pulse-ui.js +0 -257
- package/docs/public/retrowheels.css +0 -425
- package/docs/public/shop.css +0 -269
- package/docs/public/sparkys.css +0 -382
- package/docs/public/suzukibento.css +0 -517
- package/docs/public/swish.css +0 -394
- package/docs/public/theme.css +0 -52
- package/docs/public/themes/lumio.css +0 -50
- package/docs/public/themes/observatory.css +0 -104
- package/docs/server.js +0 -161
- package/docs/src/lib/component-page.js +0 -49
- package/docs/src/lib/highlight.js +0 -2
- package/docs/src/lib/layout.js +0 -190
- package/docs/src/lib/metrics-store.js +0 -6
- package/docs/src/lib/nav.js +0 -171
- package/docs/src/lib/stats.js +0 -81
- package/docs/src/pages/accessibility.js +0 -157
- package/docs/src/pages/actions.js +0 -194
- package/docs/src/pages/auth.js +0 -176
- package/docs/src/pages/caching.js +0 -95
- package/docs/src/pages/components/accordion.js +0 -48
- package/docs/src/pages/components/alert.js +0 -35
- package/docs/src/pages/components/app-badge.js +0 -41
- package/docs/src/pages/components/avatar.js +0 -35
- package/docs/src/pages/components/badge.js +0 -36
- package/docs/src/pages/components/breadcrumbs.js +0 -94
- package/docs/src/pages/components/button.js +0 -84
- package/docs/src/pages/components/card.js +0 -225
- package/docs/src/pages/components/charts.js +0 -283
- package/docs/src/pages/components/checkbox.js +0 -129
- package/docs/src/pages/components/cluster.js +0 -47
- package/docs/src/pages/components/container.js +0 -40
- package/docs/src/pages/components/cta.js +0 -54
- package/docs/src/pages/components/divider.js +0 -37
- package/docs/src/pages/components/empty.js +0 -36
- package/docs/src/pages/components/feature.js +0 -60
- package/docs/src/pages/components/fieldset.js +0 -65
- package/docs/src/pages/components/fileupload.js +0 -128
- package/docs/src/pages/components/footer.js +0 -58
- package/docs/src/pages/components/grid.js +0 -169
- package/docs/src/pages/components/heading.js +0 -107
- package/docs/src/pages/components/hero.js +0 -123
- package/docs/src/pages/components/icons.js +0 -410
- package/docs/src/pages/components/image.js +0 -71
- package/docs/src/pages/components/input.js +0 -51
- package/docs/src/pages/components/media.js +0 -51
- package/docs/src/pages/components/modal.js +0 -111
- package/docs/src/pages/components/nav.js +0 -136
- package/docs/src/pages/components/phone-frame.js +0 -74
- package/docs/src/pages/components/progress.js +0 -102
- package/docs/src/pages/components/prose.js +0 -111
- package/docs/src/pages/components/pullquote.js +0 -71
- package/docs/src/pages/components/radio.js +0 -194
- package/docs/src/pages/components/rating.js +0 -106
- package/docs/src/pages/components/search.js +0 -61
- package/docs/src/pages/components/section.js +0 -98
- package/docs/src/pages/components/segmented.js +0 -121
- package/docs/src/pages/components/select.js +0 -45
- package/docs/src/pages/components/slider.js +0 -115
- package/docs/src/pages/components/spinner.js +0 -73
- package/docs/src/pages/components/stack.js +0 -48
- package/docs/src/pages/components/stat.js +0 -55
- package/docs/src/pages/components/stepper.js +0 -66
- package/docs/src/pages/components/table.js +0 -45
- package/docs/src/pages/components/textarea.js +0 -31
- package/docs/src/pages/components/toggle.js +0 -84
- package/docs/src/pages/components/tooltip.js +0 -48
- package/docs/src/pages/components.js +0 -200
- package/docs/src/pages/config.js +0 -193
- package/docs/src/pages/constraints.js +0 -100
- package/docs/src/pages/deploy.js +0 -233
- package/docs/src/pages/error-pages.js +0 -99
- package/docs/src/pages/extending.js +0 -198
- package/docs/src/pages/faq.js +0 -99
- package/docs/src/pages/forms.js +0 -129
- package/docs/src/pages/getting-started.js +0 -147
- package/docs/src/pages/guard.js +0 -120
- package/docs/src/pages/home.js +0 -345
- package/docs/src/pages/how-it-works.js +0 -244
- package/docs/src/pages/hydration.js +0 -108
- package/docs/src/pages/images.js +0 -120
- package/docs/src/pages/markdown.js +0 -156
- package/docs/src/pages/meta.js +0 -193
- package/docs/src/pages/mutations.js +0 -107
- package/docs/src/pages/navigation.js +0 -85
- package/docs/src/pages/performance.js +0 -157
- package/docs/src/pages/persist.js +0 -89
- package/docs/src/pages/project-structure.js +0 -91
- package/docs/src/pages/prompt-examples.js +0 -186
- package/docs/src/pages/raw-responses.js +0 -122
- package/docs/src/pages/routing.js +0 -120
- package/docs/src/pages/server-api.js +0 -291
- package/docs/src/pages/server-data.js +0 -203
- package/docs/src/pages/slash-commands.js +0 -55
- package/docs/src/pages/spec.js +0 -207
- package/docs/src/pages/state.js +0 -102
- package/docs/src/pages/store.js +0 -203
- package/docs/src/pages/streaming.js +0 -107
- package/docs/src/pages/stripe.js +0 -194
- package/docs/src/pages/supabase.js +0 -324
- package/docs/src/pages/templates/andypixel.js +0 -260
- package/docs/src/pages/templates/blog-post.js +0 -226
- package/docs/src/pages/templates/bookworms.js +0 -227
- package/docs/src/pages/templates/coutumes.js +0 -237
- package/docs/src/pages/templates/journey.js +0 -186
- package/docs/src/pages/templates/mobile-app.js +0 -371
- package/docs/src/pages/templates/retrowheels.js +0 -355
- package/docs/src/pages/templates/shop.js +0 -268
- package/docs/src/pages/templates/sparkys.js +0 -264
- package/docs/src/pages/templates/suzukibento.js +0 -200
- package/docs/src/pages/templates/swish.js +0 -313
- package/docs/src/pages/templates.js +0 -534
- package/docs/src/pages/testing.js +0 -199
- package/docs/src/pages/validation.js +0 -138
- package/examples/.claude/commands/pulse-build.md +0 -7
- package/examples/.claude/commands/pulse-dev.md +0 -7
- package/examples/.claude/commands/pulse-start.md +0 -9
- package/examples/.claude/commands/pulse-stop.md +0 -5
- package/examples/.claude/commands/verify.md +0 -84
- package/examples/.claude/coverage-check.js +0 -196
- package/examples/.claude/pulse-checklist.md +0 -189
- package/examples/andypixel.css +0 -237
- package/examples/andypixel.js +0 -258
- package/examples/bookworms.css +0 -422
- package/examples/bookworms.js +0 -225
- package/examples/contact.js +0 -166
- package/examples/counter.js +0 -94
- package/examples/dev.server.js +0 -110
- package/examples/examples.test.js +0 -394
- package/examples/journey.css +0 -528
- package/examples/journey.js +0 -186
- package/examples/pricing.js +0 -262
- package/examples/products.js +0 -190
- package/examples/public/.pulse-ui-version +0 -1
- package/examples/public/pulse-ui.css +0 -3028
- package/examples/public/pulse-ui.js +0 -257
- package/examples/quiz.js +0 -208
- package/examples/retrowheels.css +0 -425
- package/examples/retrowheels.js +0 -353
- package/examples/shared.js +0 -84
- package/examples/suzukibento.css +0 -513
- package/examples/suzukibento.js +0 -200
- package/examples/swish.css +0 -394
- package/examples/swish.js +0 -311
- package/examples/todos.js +0 -162
- package/src/ui/fieldset.js +0 -37
- package/src/ui/phone-frame.js +0 -40
- package/src/ui/search.js +0 -78
- package/src/ui/segmented.js +0 -50
- package/src/ui/tooltip.js +0 -28
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
|
|
2
|
-
import { prevNext } from '../lib/nav.js'
|
|
3
|
-
import { highlight } from '../lib/highlight.js'
|
|
4
|
-
|
|
5
|
-
const { prev, next } = prevNext('/components')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/components',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Component Library — Pulse Docs',
|
|
12
|
-
description: 'Server-rendered UI components for Pulse — button, card, input, alert, stat, avatar, table and more. Fully accessible, mobile-ready, zero client JS.',
|
|
13
|
-
styles: ['/pulse-ui.css', '/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/components',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Component Library')}
|
|
21
|
-
${lead('Server-rendered building blocks. Each component is a pure function that returns an HTML string — no client-side JS, no build step, accessible and mobile-ready by default. The output is the same every time the same props are passed.')}
|
|
22
|
-
|
|
23
|
-
${section('setup', 'Setup')}
|
|
24
|
-
<p>Components are imported directly into the spec file alongside the stylesheet reference in <code>meta.styles</code>. No build step, no registration.</p>
|
|
25
|
-
${codeBlock(highlight(`import { button, card, input } from '@invisibleloop/pulse/ui'
|
|
26
|
-
import { escHtml } from '@invisibleloop/pulse/html'
|
|
27
|
-
|
|
28
|
-
export default {
|
|
29
|
-
route: '/example',
|
|
30
|
-
meta: {
|
|
31
|
-
title: 'Example',
|
|
32
|
-
styles: ['/pulse-ui.css', '/app.css'],
|
|
33
|
-
},
|
|
34
|
-
view: () => \`
|
|
35
|
-
<main id="main-content">
|
|
36
|
-
\${card({
|
|
37
|
-
title: 'Welcome',
|
|
38
|
-
content: button({ label: 'Get started', href: '/start' }),
|
|
39
|
-
})}
|
|
40
|
-
</main>
|
|
41
|
-
\`,
|
|
42
|
-
}`, 'js'))}
|
|
43
|
-
|
|
44
|
-
${section('theming', 'Theming')}
|
|
45
|
-
<p>All visual values flow through CSS custom properties. Override any <code>--ui-*</code> token in <code>:root</code> in your <code>app.css</code> to retheme the entire library at once.</p>
|
|
46
|
-
${codeBlock(highlight(`/* app.css */
|
|
47
|
-
:root {
|
|
48
|
-
--ui-accent: #6366f1;
|
|
49
|
-
--ui-accent-hover: #818cf8;
|
|
50
|
-
--ui-accent-dim: rgba(99, 102, 241, .12);
|
|
51
|
-
--ui-radius: 6px;
|
|
52
|
-
--ui-bg: #ffffff;
|
|
53
|
-
--ui-surface: #f9fafb;
|
|
54
|
-
--ui-text: #111827;
|
|
55
|
-
--ui-muted: #6b7280;
|
|
56
|
-
--ui-border: #e5e7eb;
|
|
57
|
-
}`, 'css'))}
|
|
58
|
-
<p>New variants follow the <code>ui-btn--{name}</code> CSS modifier pattern. A brand-coloured button, for example, is just a new class in <code>app.css</code> — the component itself stays untouched.</p>
|
|
59
|
-
${codeBlock(highlight(`.ui-btn--brand {
|
|
60
|
-
background: var(--brand);
|
|
61
|
-
color: #fff;
|
|
62
|
-
border: none;
|
|
63
|
-
}
|
|
64
|
-
.ui-btn--brand:hover:not(.ui-btn--disabled) {
|
|
65
|
-
background: var(--brand-hover);
|
|
66
|
-
}`, 'css'))}
|
|
67
|
-
|
|
68
|
-
${section('components', 'Components')}
|
|
69
|
-
<p>Each component has its own page with full demos, code examples, and a props reference.</p>
|
|
70
|
-
|
|
71
|
-
<h3 class="doc-h3">UI</h3>
|
|
72
|
-
${table(
|
|
73
|
-
['Component', 'Description'],
|
|
74
|
-
[
|
|
75
|
-
['<a href="/components/button">button</a>', 'Renders as <code><button></code> or <code><a></code>. Four variants, three sizes.'],
|
|
76
|
-
['<a href="/components/badge">badge</a>', 'Inline status label. Five semantic colour variants.'],
|
|
77
|
-
['<a href="/components/card">card</a>', 'Content surface with title, body, and optional footer.'],
|
|
78
|
-
['<a href="/components/input">input</a>', 'Labelled text input with hint and error support.'],
|
|
79
|
-
['<a href="/components/fieldset">fieldset</a>', 'Semantic grouping of related fields with an accessible legend.'],
|
|
80
|
-
['<a href="/components/select">select</a>', 'Styled select with option groups and current-value support.'],
|
|
81
|
-
['<a href="/components/textarea">textarea</a>', 'Multi-line input with label, hint, and error.'],
|
|
82
|
-
['<a href="/components/alert">alert</a>', 'Inline feedback banner. ARIA roles wired by variant.'],
|
|
83
|
-
['<a href="/components/stat">stat</a>', 'Numeric metric with optional trend arrow.'],
|
|
84
|
-
['<a href="/components/avatar">avatar</a>', 'User avatar — image with fallback to initials.'],
|
|
85
|
-
['<a href="/components/empty">empty</a>', 'Empty state with title, description, and optional CTA.'],
|
|
86
|
-
['<a href="/components/table">table</a>', 'Accessible data table with scroll wrapper.'],
|
|
87
|
-
]
|
|
88
|
-
)}
|
|
89
|
-
|
|
90
|
-
<h3 class="doc-h3">Landing page</h3>
|
|
91
|
-
${table(
|
|
92
|
-
['Component', 'Description'],
|
|
93
|
-
[
|
|
94
|
-
['<a href="/components/nav">nav</a>', 'Site header with logo, links, and optional CTA.'],
|
|
95
|
-
['<a href="/components/hero">hero</a>', 'Full-width hero section with eyebrow, title, and actions.'],
|
|
96
|
-
['<a href="/components/app-badge">appBadge</a>', 'App Store / Google Play download badge.'],
|
|
97
|
-
['<a href="/components/feature">feature</a>', 'Icon + title + description block for feature grids.'],
|
|
98
|
-
['<a href="/components/accordion">accordion</a>', 'Collapsible FAQ items — no JS, native <code><details></code>.'],
|
|
99
|
-
['<a href="/components/cta">cta</a>', 'Call-to-action block with eyebrow, heading, body, and actions slot.'],
|
|
100
|
-
]
|
|
101
|
-
)}
|
|
102
|
-
|
|
103
|
-
<h3 class="doc-h3">Layout</h3>
|
|
104
|
-
${table(
|
|
105
|
-
['Component', 'Description'],
|
|
106
|
-
[
|
|
107
|
-
['<a href="/components/container">container</a>', 'Max-width wrapper with horizontal padding.'],
|
|
108
|
-
['<a href="/components/section">section</a>', 'Vertical padding block with background variant.'],
|
|
109
|
-
['<a href="/components/grid">grid</a>', 'Responsive CSS grid. Collapses to one column on mobile.'],
|
|
110
|
-
['<a href="/components/stack">stack</a>', 'Flex column with consistent vertical gap.'],
|
|
111
|
-
['<a href="/components/cluster">cluster</a>', 'Flex row with wrapping — for badges, buttons, etc.'],
|
|
112
|
-
['<a href="/components/divider">divider</a>', 'Horizontal rule, optionally with centred label.'],
|
|
113
|
-
['<a href="/components/media">media</a>', 'Two-column image + text layout, stacks on mobile.'],
|
|
114
|
-
['<a href="/components/footer">footer</a>', 'Site footer with logo, nav links, and legal text. Stacks on mobile.'],
|
|
115
|
-
]
|
|
116
|
-
)}
|
|
117
|
-
|
|
118
|
-
${section('utilities', 'Utility classes')}
|
|
119
|
-
<p><code>pulse-ui.css</code> ships a utility layer (prefix <code>u-</code>) for common spacing, typography, and layout needs. Reach for these before writing custom CSS — they use the same <code>--ui-*</code> tokens as components so theme overrides apply everywhere.</p>
|
|
120
|
-
|
|
121
|
-
<h3 class="doc-h3">Spacing</h3>
|
|
122
|
-
<p>Scale: 1=4px 2=8px 3=12px 4=16px 5=20px 6=24px 8=32px 10=40px 12=48px 16=64px</p>
|
|
123
|
-
${table(
|
|
124
|
-
['Class', 'Property'],
|
|
125
|
-
[
|
|
126
|
-
['<code>u-mt-{0–16}</code>', 'margin-top'],
|
|
127
|
-
['<code>u-mb-{0–16}</code>', 'margin-bottom'],
|
|
128
|
-
['<code>u-mx-auto</code>', 'margin-left + right: auto'],
|
|
129
|
-
['<code>u-p-{0–8}</code>', 'padding (all sides)'],
|
|
130
|
-
['<code>u-px-{0–8}</code>', 'padding-left + right'],
|
|
131
|
-
['<code>u-py-{0–8}</code>', 'padding-top + bottom'],
|
|
132
|
-
]
|
|
133
|
-
)}
|
|
134
|
-
|
|
135
|
-
<h3 class="doc-h3">Typography</h3>
|
|
136
|
-
${table(
|
|
137
|
-
['Class', 'Effect'],
|
|
138
|
-
[
|
|
139
|
-
['<code>u-text-{xs,sm,base,lg,xl,2xl,3xl,4xl}</code>', 'Font size + matching line-height'],
|
|
140
|
-
['<code>u-font-{normal,medium,semibold,bold}</code>', 'Font weight'],
|
|
141
|
-
['<code>u-text-{left,center,right}</code>', 'Text alignment'],
|
|
142
|
-
['<code>u-text-{default,muted,accent,green,red,yellow,blue}</code>', 'Token colour'],
|
|
143
|
-
['<code>u-leading-{tight,snug,normal,relaxed,loose}</code>', 'Line height'],
|
|
144
|
-
]
|
|
145
|
-
)}
|
|
146
|
-
|
|
147
|
-
<h3 class="doc-h3">Layout</h3>
|
|
148
|
-
${table(
|
|
149
|
-
['Class', 'Effect'],
|
|
150
|
-
[
|
|
151
|
-
['<code>u-flex</code> / <code>u-flex-col</code>', 'Flex row or column'],
|
|
152
|
-
['<code>u-items-{start,center,end,stretch}</code>', 'align-items'],
|
|
153
|
-
['<code>u-justify-{start,center,end,between}</code>', 'justify-content'],
|
|
154
|
-
['<code>u-gap-{1–8}</code>', 'gap'],
|
|
155
|
-
['<code>u-w-full</code>', 'width: 100%'],
|
|
156
|
-
['<code>u-max-w-{xs,sm,md,lg,xl,prose}</code>', 'max-width (320px–1024px, 65ch)'],
|
|
157
|
-
['<code>u-hidden</code> / <code>u-block</code> / <code>u-inline-block</code>', 'display'],
|
|
158
|
-
]
|
|
159
|
-
)}
|
|
160
|
-
|
|
161
|
-
<h3 class="doc-h3">Visual</h3>
|
|
162
|
-
${table(
|
|
163
|
-
['Class', 'Effect'],
|
|
164
|
-
[
|
|
165
|
-
['<code>u-rounded</code> / <code>u-rounded-md</code> / <code>u-rounded-lg</code> / <code>u-rounded-full</code>', 'border-radius'],
|
|
166
|
-
['<code>u-border</code> / <code>u-border-t</code> / <code>u-border-b</code>', '1px solid --ui-border'],
|
|
167
|
-
['<code>u-bg-surface</code> / <code>u-bg-surface2</code> / <code>u-bg-accent</code>', 'background token'],
|
|
168
|
-
['<code>u-overflow-hidden</code> / <code>u-overflow-auto</code>', 'overflow'],
|
|
169
|
-
['<code>u-opacity-50</code> / <code>u-opacity-75</code>', 'opacity'],
|
|
170
|
-
]
|
|
171
|
-
)}
|
|
172
|
-
|
|
173
|
-
<p>Utilities compose naturally with components and with each other:</p>
|
|
174
|
-
${codeBlock(highlight(`<!-- centred hero block — no custom CSS needed -->
|
|
175
|
-
<div class="u-flex u-flex-col u-items-center u-text-center u-py-16 u-gap-4">
|
|
176
|
-
<h1 class="u-text-4xl u-font-bold">Hello</h1>
|
|
177
|
-
<p class="u-text-lg u-text-muted u-max-w-prose">Subtitle goes here.</p>
|
|
178
|
-
\${button({ label: 'Get started', href: '/start' })}
|
|
179
|
-
</div>`, 'html'))}
|
|
180
|
-
|
|
181
|
-
${callout('note', 'Never use inline <code>style=""</code> attributes. Reach for utility classes first, and only add to <code>app.css</code> when you need something utilities cannot provide — a unique animation, a custom grid, or a one-off component variant.')}
|
|
182
|
-
|
|
183
|
-
${section('composing', 'Composing components')}
|
|
184
|
-
<p>Components compose naturally — pass the output of one as the <code>content</code> or <code>footer</code> of another. Here's a stat dashboard card:</p>
|
|
185
|
-
${codeBlock(highlight(`import { card, stat, button } from '@invisibleloop/pulse/ui'
|
|
186
|
-
|
|
187
|
-
card({
|
|
188
|
-
title: 'This week',
|
|
189
|
-
content: \`
|
|
190
|
-
\${stat({ label: 'Page views', value: '48,291', change: '+12%', trend: 'up' })}
|
|
191
|
-
\${stat({ label: 'New users', value: '1,042', change: '+4%', trend: 'up' })}
|
|
192
|
-
\${stat({ label: 'Bounced', value: '22%', change: '+1%', trend: 'down' })}
|
|
193
|
-
\`,
|
|
194
|
-
footer: button({ label: 'View full report', href: '/analytics', variant: 'ghost', size: 'sm' }),
|
|
195
|
-
})`, 'js'))}
|
|
196
|
-
|
|
197
|
-
${callout('note', 'Text props like <code>label</code>, <code>title</code>, <code>value</code>, and <code>error</code> are escaped automatically. <code>content</code>, <code>footer</code>, <code>rows</code>, <code>actions</code>, <code>icon</code>, <code>action</code>, and <code>logo</code> are raw HTML slots — they pass through as-is, so any user-supplied data going into those slots should go through <code>escHtml()</code> first.')}
|
|
198
|
-
`,
|
|
199
|
-
}),
|
|
200
|
-
}
|
package/docs/src/pages/config.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
|
|
2
|
-
import { prevNext } from '../lib/nav.js'
|
|
3
|
-
import { highlight } from '../lib/highlight.js'
|
|
4
|
-
|
|
5
|
-
const { prev, next } = prevNext('/config')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/config',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Configuration — Pulse Docs',
|
|
12
|
-
description: 'Full reference for pulse.config.js — port, Lighthouse thresholds, load test config, environments, and per-route overrides.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/config',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Configuration')}
|
|
21
|
-
${lead('<code>pulse.config.js</code> sets the performance and load thresholds that Pulse enforces across your application. All fields are optional — the defaults match Google\'s Core Web Vitals "good" band. Configuration here is not about enabling features; it is about deciding where, if anywhere, you need to lower a guaranteed baseline.')}
|
|
22
|
-
|
|
23
|
-
${section('schema', 'Full schema')}
|
|
24
|
-
${codeBlock(highlight(`// pulse.config.js
|
|
25
|
-
export default {
|
|
26
|
-
port: 3000,
|
|
27
|
-
|
|
28
|
-
lighthouse: {
|
|
29
|
-
// Category scores — 0 to 100. Default: 100 for all four.
|
|
30
|
-
performance: 100,
|
|
31
|
-
accessibility: 100,
|
|
32
|
-
bestPractices: 100,
|
|
33
|
-
seo: 100,
|
|
34
|
-
|
|
35
|
-
// Core Web Vitals and timing metrics.
|
|
36
|
-
// Defaults are the Google "good" thresholds.
|
|
37
|
-
lcp: 2500, // Largest Contentful Paint (ms)
|
|
38
|
-
cls: 0.1, // Cumulative Layout Shift
|
|
39
|
-
tbt: 200, // Total Blocking Time (ms)
|
|
40
|
-
fcp: 1800, // First Contentful Paint (ms)
|
|
41
|
-
si: 3400, // Speed Index (ms)
|
|
42
|
-
inp: 200, // Interaction to Next Paint (ms)
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
load: {
|
|
46
|
-
duration: 10,
|
|
47
|
-
connections: 10,
|
|
48
|
-
thresholds: {
|
|
49
|
-
rps: undefined, // minimum req/s (optional)
|
|
50
|
-
p99: undefined, // maximum p99 latency ms (optional)
|
|
51
|
-
errors: 0,
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
environments: {
|
|
56
|
-
// Environment names are bespoke — choose whatever suits your project.
|
|
57
|
-
local: { url: 'http://localhost:3000', default: true },
|
|
58
|
-
staging: {
|
|
59
|
-
url: 'https://staging.myapp.com',
|
|
60
|
-
headers: { Authorization: \`Bearer \${process.env.STAGING_TOKEN}\` },
|
|
61
|
-
load: { duration: 30, connections: 50 },
|
|
62
|
-
lighthouse: { performance: 90 },
|
|
63
|
-
},
|
|
64
|
-
production: { url: 'https://myapp.com' },
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
routes: {
|
|
68
|
-
// Per-route overrides — merged on top of global lighthouse/load config.
|
|
69
|
-
// Only specify what differs from the global defaults.
|
|
70
|
-
'/dashboard': {
|
|
71
|
-
lighthouse: {
|
|
72
|
-
performance: 85,
|
|
73
|
-
lcp: 4000,
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
'/embed': {
|
|
77
|
-
lighthouse: {
|
|
78
|
-
bestPractices: 85,
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
}`, 'js'))}
|
|
83
|
-
|
|
84
|
-
${section('port', 'port')}
|
|
85
|
-
${table(
|
|
86
|
-
['Field', 'Type', 'Default', 'Description'],
|
|
87
|
-
[
|
|
88
|
-
['<code>port</code>', '<code>number</code>', '<code>3000</code>', 'Port the dev and production servers listen on.'],
|
|
89
|
-
]
|
|
90
|
-
)}
|
|
91
|
-
|
|
92
|
-
${section('lighthouse', 'lighthouse')}
|
|
93
|
-
<p>Global Lighthouse thresholds that every page must meet. <code>/pulse-report</code> enforces these after every audit — any page that falls below a threshold is reported as a failure, not a warning.</p>
|
|
94
|
-
${table(
|
|
95
|
-
['Field', 'Type', 'Default', 'Description'],
|
|
96
|
-
[
|
|
97
|
-
['<code>performance</code>', '<code>number</code>', '<code>100</code>', 'Lighthouse Performance category score (0–100).'],
|
|
98
|
-
['<code>accessibility</code>', '<code>number</code>', '<code>100</code>', 'Lighthouse Accessibility category score (0–100).'],
|
|
99
|
-
['<code>bestPractices</code>', '<code>number</code>', '<code>100</code>', 'Lighthouse Best Practices category score (0–100).'],
|
|
100
|
-
['<code>seo</code>', '<code>number</code>', '<code>100</code>', 'Lighthouse SEO category score (0–100).'],
|
|
101
|
-
['<code>lcp</code>', '<code>number</code>', '<code>2500</code>', 'Largest Contentful Paint budget (ms).'],
|
|
102
|
-
['<code>cls</code>', '<code>number</code>', '<code>0.1</code>', 'Cumulative Layout Shift budget.'],
|
|
103
|
-
['<code>tbt</code>', '<code>number</code>', '<code>200</code>', 'Total Blocking Time budget (ms).'],
|
|
104
|
-
['<code>fcp</code>', '<code>number</code>', '<code>1800</code>', 'First Contentful Paint budget (ms).'],
|
|
105
|
-
['<code>si</code>', '<code>number</code>', '<code>3400</code>', 'Speed Index budget (ms).'],
|
|
106
|
-
['<code>inp</code>', '<code>number</code>', '<code>200</code>', 'Interaction to Next Paint budget (ms).'],
|
|
107
|
-
]
|
|
108
|
-
)}
|
|
109
|
-
${callout('note', 'Unset fields default to the values above. Setting a field to <code>null</code> removes that check for the project — use sparingly. Raising a threshold is always preferable to disabling it.')}
|
|
110
|
-
|
|
111
|
-
${section('routes', 'routes')}
|
|
112
|
-
<p>Route-specific overrides let you lower a threshold for a specific page without relaxing the global guarantee. Only specify the fields that differ — everything else inherits from the global config.</p>
|
|
113
|
-
${codeBlock(highlight(`routes: {
|
|
114
|
-
// This route uses a third-party chart library — relax performance only.
|
|
115
|
-
'/dashboard': {
|
|
116
|
-
lighthouse: {
|
|
117
|
-
performance: 85,
|
|
118
|
-
lcp: 4000,
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
}`, 'js'))}
|
|
122
|
-
${callout('note', 'Route keys must exactly match the spec <code>route</code> field, including any leading slash. Dynamic segments are not supported — create a specific override for each route pattern.')}
|
|
123
|
-
|
|
124
|
-
${section('load', 'load')}
|
|
125
|
-
<p>Load test thresholds enforced by <code>/pulse-load</code>. All fields are optional — omitting a threshold means that check is not enforced.</p>
|
|
126
|
-
${table(
|
|
127
|
-
['Field', 'Type', 'Default', 'Description'],
|
|
128
|
-
[
|
|
129
|
-
['<code>duration</code>', '<code>number</code>', '<code>10</code>', 'Test duration in seconds.'],
|
|
130
|
-
['<code>connections</code>', '<code>number</code>', '<code>10</code>', 'Number of concurrent request chains.'],
|
|
131
|
-
['<code>thresholds.rps</code>', '<code>number</code>', '<code>undefined</code>', 'Minimum acceptable requests per second. Unset = no check.'],
|
|
132
|
-
['<code>thresholds.p99</code>', '<code>number</code>', '<code>undefined</code>', 'Maximum acceptable p99 latency (ms). Unset = no check.'],
|
|
133
|
-
['<code>thresholds.errors</code>', '<code>number</code>', '<code>0</code>', 'Maximum acceptable error count.'],
|
|
134
|
-
]
|
|
135
|
-
)}
|
|
136
|
-
${codeBlock(highlight(`load: {
|
|
137
|
-
duration: 30,
|
|
138
|
-
connections: 20,
|
|
139
|
-
thresholds: {
|
|
140
|
-
rps: 100, // fail if below 100 req/s
|
|
141
|
-
p99: 500, // fail if p99 exceeds 500ms
|
|
142
|
-
errors: 0,
|
|
143
|
-
},
|
|
144
|
-
},`, 'js'))}
|
|
145
|
-
<p>Per-route overrides follow the same pattern as <code>lighthouse</code>:</p>
|
|
146
|
-
${codeBlock(highlight(`routes: {
|
|
147
|
-
'/feed': {
|
|
148
|
-
load: { connections: 5, thresholds: { rps: 20 } },
|
|
149
|
-
},
|
|
150
|
-
}`, 'js'))}
|
|
151
|
-
<p>Results are saved to <code>.pulse/load-reports/</code> and displayed in the Load Tests tab of the report dashboard, alongside the Lighthouse Performance tab for the same route.</p>
|
|
152
|
-
|
|
153
|
-
${section('environments', 'environments')}
|
|
154
|
-
<p>Named environments let you enforce thresholds against different targets — local, staging, production — from the same config. Thresholds are applied per-environment, so staging and production can have different performance floors.</p>
|
|
155
|
-
${table(
|
|
156
|
-
['Field', 'Type', 'Description'],
|
|
157
|
-
[
|
|
158
|
-
['<code>url</code>', '<code>string</code>', 'Base URL to test against. If it contains <code>localhost</code> or <code>127.0.0.1</code>, a local production build is spun up automatically. Remote URLs are tested directly.'],
|
|
159
|
-
['<code>default</code>', '<code>boolean</code>', 'The environment used when none is explicitly specified by <code>pulse report</code> and <code>pulse load-test</code>.'],
|
|
160
|
-
['<code>headers</code>', '<code>object</code>', 'HTTP headers sent with every request to this environment. Useful for authorization tokens on protected staging deployments — read values from <code>process.env</code> rather than hardcoding them.'],
|
|
161
|
-
['<code>load</code>', '<code>object</code>', 'Load test config overrides for this environment. Same fields as the global <code>load</code> block. Merged on top of global config.'],
|
|
162
|
-
['<code>lighthouse</code>', '<code>object</code>', 'Lighthouse threshold overrides for this environment. Same fields as the global <code>lighthouse</code> block. Merged on top of global config.'],
|
|
163
|
-
]
|
|
164
|
-
)}
|
|
165
|
-
${callout('note', 'Environment names are fully bespoke — <code>local</code>, <code>staging</code>, <code>production</code>, <code>preview</code>, <code>eu</code> — whatever maps to your project\'s infrastructure. There are no reserved names.')}
|
|
166
|
-
<p>Threshold merge order: <strong>global config → environment override → per-route override</strong>.</p>
|
|
167
|
-
${codeBlock(highlight(`environments: {
|
|
168
|
-
local: { url: 'http://localhost:3000', default: true },
|
|
169
|
-
staging: {
|
|
170
|
-
url: 'https://staging.myapp.com',
|
|
171
|
-
headers: { Authorization: \`Bearer \${process.env.STAGING_TOKEN}\` },
|
|
172
|
-
load: { duration: 30, connections: 50, thresholds: { rps: 500 } },
|
|
173
|
-
lighthouse: { performance: 90 },
|
|
174
|
-
},
|
|
175
|
-
production: { url: 'https://myapp.com' },
|
|
176
|
-
}`, 'js'))}
|
|
177
|
-
|
|
178
|
-
${section('defaults', 'CWV default thresholds')}
|
|
179
|
-
<p>The default metric thresholds match the Google "good" band from the Core Web Vitals specification:</p>
|
|
180
|
-
${table(
|
|
181
|
-
['Metric', 'Default', 'Google "good" threshold'],
|
|
182
|
-
[
|
|
183
|
-
['LCP', '<code>2500 ms</code>', '≤ 2500 ms'],
|
|
184
|
-
['CLS', '<code>0.1</code>', '≤ 0.1'],
|
|
185
|
-
['TBT', '<code>200 ms</code>', '≤ 200 ms'],
|
|
186
|
-
['FCP', '<code>1800 ms</code>', '≤ 1800 ms'],
|
|
187
|
-
['SI', '<code>3400 ms</code>', '≤ 3400 ms'],
|
|
188
|
-
['INP', '<code>200 ms</code>', '≤ 200 ms'],
|
|
189
|
-
]
|
|
190
|
-
)}
|
|
191
|
-
`,
|
|
192
|
-
}),
|
|
193
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead, section, sub, codeBlock, callout, table } from '../lib/layout.js'
|
|
2
|
-
import { prevNext } from '../lib/nav.js'
|
|
3
|
-
import { highlight } from '../lib/highlight.js'
|
|
4
|
-
|
|
5
|
-
const { prev, next } = prevNext('/constraints')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/constraints',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Constraints — Pulse Docs',
|
|
12
|
-
description: 'Automatic min/max bounds on state values, enforced after every mutation.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
state: {},
|
|
16
|
-
view: () => renderLayout({
|
|
17
|
-
currentHref: '/constraints',
|
|
18
|
-
prev,
|
|
19
|
-
next,
|
|
20
|
-
content: `
|
|
21
|
-
${h1('Constraints')}
|
|
22
|
-
${lead('Constraints are always-on bounds for numeric state values. After every mutation, Pulse clamps constrained values to their declared range before the view re-renders. The value can never go out of range — there is no code path where it can.')}
|
|
23
|
-
|
|
24
|
-
${section('declaring', 'Declaring constraints')}
|
|
25
|
-
<p>The <code>constraints</code> field maps top-level state keys to bounds objects with optional <code>min</code> and <code>max</code> properties:</p>
|
|
26
|
-
${codeBlock(highlight(`export default {
|
|
27
|
-
route: '/cart',
|
|
28
|
-
state: {
|
|
29
|
-
quantity: 1,
|
|
30
|
-
zoom: 1.0,
|
|
31
|
-
rating: 0,
|
|
32
|
-
},
|
|
33
|
-
constraints: {
|
|
34
|
-
quantity: { min: 1, max: 99 },
|
|
35
|
-
zoom: { min: 0.5, max: 3.0 },
|
|
36
|
-
rating: { min: 0, max: 5 },
|
|
37
|
-
},
|
|
38
|
-
mutations: {
|
|
39
|
-
increaseQty: (state) => ({ quantity: state.quantity + 1 }),
|
|
40
|
-
decreaseQty: (state) => ({ quantity: state.quantity - 1 }),
|
|
41
|
-
zoomIn: (state) => ({ zoom: state.zoom + 0.1 }),
|
|
42
|
-
zoomOut: (state) => ({ zoom: state.zoom - 0.1 }),
|
|
43
|
-
},
|
|
44
|
-
}`, 'js'))}
|
|
45
|
-
<p>When <code>decreaseQty</code> runs and <code>quantity</code> is already 1, the constraint clamps it back to 1 before the view renders. The mutation does not need to check bounds — the spec declares them once and Pulse enforces them everywhere.</p>
|
|
46
|
-
|
|
47
|
-
${section('vs-validation', 'Constraints vs Validation')}
|
|
48
|
-
${table(
|
|
49
|
-
['Aspect', 'Constraints', 'Validation'],
|
|
50
|
-
[
|
|
51
|
-
['When it runs', 'After <strong>every</strong> mutation, automatically', 'Only when an action has <code>validate: true</code>'],
|
|
52
|
-
['What it does', 'Clamps numeric values silently', 'Rejects the action and surfaces errors'],
|
|
53
|
-
['User feedback', 'None — state is silently corrected', 'Explicit error messages shown in the view'],
|
|
54
|
-
['Best for', 'Numeric ranges that must never be exceeded', 'Form field correctness before submission'],
|
|
55
|
-
]
|
|
56
|
-
)}
|
|
57
|
-
|
|
58
|
-
${callout('note', 'Constraints and validation serve different purposes. Constraints silently enforce numeric bounds at every mutation — they cannot be bypassed. Validation rejects invalid form data before an action\'s async work begins — it only runs when explicitly declared.')}
|
|
59
|
-
|
|
60
|
-
${section('one-sided', 'One-sided bounds')}
|
|
61
|
-
<p>Either <code>min</code> or <code>max</code> can be declared alone — both are optional:</p>
|
|
62
|
-
${codeBlock(highlight(`constraints: {
|
|
63
|
-
count: { min: 0 }, // no upper limit
|
|
64
|
-
discount: { max: 100 }, // no lower limit
|
|
65
|
-
offset: { min: 0, max: 999 } // both bounds
|
|
66
|
-
}`, 'js'))}
|
|
67
|
-
|
|
68
|
-
${section('how-it-works', 'How clamping works')}
|
|
69
|
-
<p>After Pulse applies a mutation's partial state update, it iterates over all declared constraints and applies <code>Math.max(min, Math.min(max, value))</code> to each constrained key. The view is then called with the clamped state.</p>
|
|
70
|
-
${codeBlock(highlight(`// state.count = 10, constraints.count = { min: 0, max: 10 }
|
|
71
|
-
mutations: {
|
|
72
|
-
increment: (state) => ({ count: state.count + 1 }),
|
|
73
|
-
// mutation returns { count: 11 }
|
|
74
|
-
// constraint clamps to 10
|
|
75
|
-
// view receives { count: 10 }
|
|
76
|
-
}`, 'js'))}
|
|
77
|
-
|
|
78
|
-
${section('top-level', 'Top-level keys only')}
|
|
79
|
-
<p>Constraints apply to top-level state keys. To constrain nested values, consider flattening your state structure or applying bounds logic in the mutation itself:</p>
|
|
80
|
-
${codeBlock(highlight(`// Cannot do:
|
|
81
|
-
constraints: {
|
|
82
|
-
'player.health': { min: 0, max: 100 } // ✗ nested paths not supported
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Do instead:
|
|
86
|
-
state: { playerHealth: 100 },
|
|
87
|
-
constraints: { playerHealth: { min: 0, max: 100 } },
|
|
88
|
-
|
|
89
|
-
// Or handle in the mutation:
|
|
90
|
-
mutations: {
|
|
91
|
-
takeDamage: (state, _, amount) => ({
|
|
92
|
-
player: {
|
|
93
|
-
...state.player,
|
|
94
|
-
health: Math.max(0, state.player.health - amount),
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
}`, 'js'))}
|
|
98
|
-
`,
|
|
99
|
-
}),
|
|
100
|
-
}
|