@invisibleloop/pulse 0.1.21
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/.claude/commands/build-page.md +59 -0
- package/.claude/commands/new-doc-page.md +45 -0
- package/.claude/commands/verify.md +52 -0
- package/.claude/pulse-checklist.md +111 -0
- package/.claude/settings.local.json +102 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +41 -0
- package/.pulse/load-reports/home/1773432711417.json +22 -0
- package/CLAUDE.md +383 -0
- package/README.md +95 -0
- package/docs/.claude/pulse-checklist.md +111 -0
- package/docs/public/.pulse-ui-version +1 -0
- package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
- package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
- package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
- package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
- package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
- package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
- package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
- package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
- package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
- package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
- package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
- package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
- package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
- package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
- package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
- package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
- package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
- package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
- package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
- package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
- package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
- package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
- package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
- package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
- package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
- package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
- package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
- package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
- package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
- package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
- package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
- package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
- package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
- package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
- package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
- package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
- package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
- package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
- package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
- package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
- package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
- package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
- package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
- package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
- package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
- package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
- package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
- package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
- package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
- package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
- package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
- package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
- package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
- package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
- package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
- package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
- package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
- package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
- package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
- package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
- package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
- package/docs/public/dist/docs-8e3d4b5c.css +1 -0
- package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
- package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
- package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
- package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
- package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
- package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
- package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
- package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
- package/docs/public/dist/manifest.json +94 -0
- package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
- package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
- package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
- package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
- package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
- package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
- package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
- package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
- package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
- package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
- package/docs/public/dist/runtime-B73WLANC.js +1 -0
- package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
- package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
- package/docs/public/dist/runtime-QFURDKA2.js +5 -0
- package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
- package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
- package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
- package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
- package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
- package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
- package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
- package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
- package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
- package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
- package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
- package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
- package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
- package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
- package/docs/public/docs.css +2020 -0
- package/docs/public/menu.js +83 -0
- package/docs/public/pulse-ui.css +2739 -0
- package/docs/public/pulse-ui.js +236 -0
- package/docs/server.js +192 -0
- package/docs/src/lib/component-page.js +47 -0
- package/docs/src/lib/highlight.js +255 -0
- package/docs/src/lib/layout.js +131 -0
- package/docs/src/lib/metrics-store.js +6 -0
- package/docs/src/lib/nav.js +159 -0
- package/docs/src/lib/stats.js +81 -0
- package/docs/src/pages/accessibility.js +157 -0
- package/docs/src/pages/actions.js +191 -0
- package/docs/src/pages/auth.js +177 -0
- package/docs/src/pages/caching.js +95 -0
- package/docs/src/pages/components/accordion.js +48 -0
- package/docs/src/pages/components/alert.js +35 -0
- package/docs/src/pages/components/app-badge.js +41 -0
- package/docs/src/pages/components/avatar.js +35 -0
- package/docs/src/pages/components/badge.js +36 -0
- package/docs/src/pages/components/banner.js +45 -0
- package/docs/src/pages/components/breadcrumbs.js +94 -0
- package/docs/src/pages/components/button.js +84 -0
- package/docs/src/pages/components/card.js +225 -0
- package/docs/src/pages/components/carousel.js +72 -0
- package/docs/src/pages/components/charts.js +278 -0
- package/docs/src/pages/components/checkbox.js +129 -0
- package/docs/src/pages/components/cluster.js +47 -0
- package/docs/src/pages/components/code-window.js +57 -0
- package/docs/src/pages/components/container.js +40 -0
- package/docs/src/pages/components/cta.js +53 -0
- package/docs/src/pages/components/divider.js +37 -0
- package/docs/src/pages/components/empty.js +36 -0
- package/docs/src/pages/components/feature.js +60 -0
- package/docs/src/pages/components/fieldset.js +65 -0
- package/docs/src/pages/components/fileupload.js +127 -0
- package/docs/src/pages/components/footer.js +58 -0
- package/docs/src/pages/components/grid.js +165 -0
- package/docs/src/pages/components/heading.js +107 -0
- package/docs/src/pages/components/hero.js +65 -0
- package/docs/src/pages/components/icons.js +285 -0
- package/docs/src/pages/components/image.js +71 -0
- package/docs/src/pages/components/input.js +51 -0
- package/docs/src/pages/components/list.js +112 -0
- package/docs/src/pages/components/media.js +51 -0
- package/docs/src/pages/components/modal.js +111 -0
- package/docs/src/pages/components/nav.js +86 -0
- package/docs/src/pages/components/pricing.js +68 -0
- package/docs/src/pages/components/progress.js +102 -0
- package/docs/src/pages/components/prose.js +111 -0
- package/docs/src/pages/components/pullquote.js +71 -0
- package/docs/src/pages/components/radio.js +194 -0
- package/docs/src/pages/components/rating.js +106 -0
- package/docs/src/pages/components/search.js +61 -0
- package/docs/src/pages/components/section.js +59 -0
- package/docs/src/pages/components/segmented.js +121 -0
- package/docs/src/pages/components/select.js +45 -0
- package/docs/src/pages/components/slider.js +114 -0
- package/docs/src/pages/components/spinner.js +73 -0
- package/docs/src/pages/components/stack.js +48 -0
- package/docs/src/pages/components/stat.js +55 -0
- package/docs/src/pages/components/stepper.js +66 -0
- package/docs/src/pages/components/table.js +45 -0
- package/docs/src/pages/components/testimonial.js +49 -0
- package/docs/src/pages/components/textarea.js +31 -0
- package/docs/src/pages/components/timeline.js +227 -0
- package/docs/src/pages/components/toggle.js +84 -0
- package/docs/src/pages/components/tooltip.js +48 -0
- package/docs/src/pages/components.js +204 -0
- package/docs/src/pages/config.js +193 -0
- package/docs/src/pages/constraints.js +99 -0
- package/docs/src/pages/deploy.js +233 -0
- package/docs/src/pages/extending.js +198 -0
- package/docs/src/pages/faq.js +96 -0
- package/docs/src/pages/getting-started.js +106 -0
- package/docs/src/pages/guard.js +121 -0
- package/docs/src/pages/home.js +401 -0
- package/docs/src/pages/how-it-works.js +183 -0
- package/docs/src/pages/hydration.js +98 -0
- package/docs/src/pages/images.js +121 -0
- package/docs/src/pages/meta.js +120 -0
- package/docs/src/pages/mutations.js +106 -0
- package/docs/src/pages/navigation.js +85 -0
- package/docs/src/pages/performance.js +157 -0
- package/docs/src/pages/persist.js +88 -0
- package/docs/src/pages/project-structure.js +90 -0
- package/docs/src/pages/prompt-examples.js +186 -0
- package/docs/src/pages/raw-responses.js +124 -0
- package/docs/src/pages/routing.js +99 -0
- package/docs/src/pages/server-api.js +281 -0
- package/docs/src/pages/server-data.js +185 -0
- package/docs/src/pages/slash-commands.js +55 -0
- package/docs/src/pages/spec.js +207 -0
- package/docs/src/pages/state.js +101 -0
- package/docs/src/pages/store.js +181 -0
- package/docs/src/pages/streaming.js +108 -0
- package/docs/src/pages/stripe.js +193 -0
- package/docs/src/pages/supabase.js +323 -0
- package/docs/src/pages/testing.js +198 -0
- package/docs/src/pages/validation.js +138 -0
- package/examples/contact.js +166 -0
- package/examples/counter.js +94 -0
- package/examples/dev.server.js +91 -0
- package/examples/examples.test.js +394 -0
- package/examples/pricing.js +244 -0
- package/examples/products.js +191 -0
- package/examples/quiz.js +208 -0
- package/examples/shared.js +78 -0
- package/examples/todos.js +162 -0
- package/package.json +75 -0
- package/public/.pulse-ui-version +1 -0
- package/public/chippy-bird.css +246 -0
- package/public/examples/contact.css +119 -0
- package/public/examples/counter.css +79 -0
- package/public/examples/pricing.css +132 -0
- package/public/examples/products.css +100 -0
- package/public/examples/quiz.css +200 -0
- package/public/examples/todos.css +137 -0
- package/public/favicon.ico +0 -0
- package/public/log-dashboard.css +383 -0
- package/public/pulse-ui.css +2740 -0
- package/public/pulse-ui.js +236 -0
- package/public/pulse.css +149 -0
- package/scripts/build.js +411 -0
- package/src/agent/checklist.md +111 -0
- package/src/agent/coverage-check.js +66 -0
- package/src/agent/guide-components.md +274 -0
- package/src/agent/guide-examples.md +54 -0
- package/src/agent/guide-routing.md +36 -0
- package/src/agent/guide-server.md +258 -0
- package/src/agent/guide-spec.md +103 -0
- package/src/agent/guide-styles.md +191 -0
- package/src/agent/guide.md +979 -0
- package/src/agent/identity.md +106 -0
- package/src/agent/workflow.md +108 -0
- package/src/cli/cli.test.js +82 -0
- package/src/cli/dev.js +195 -0
- package/src/cli/discover.js +113 -0
- package/src/cli/index.js +361 -0
- package/src/cli/load-report.js +91 -0
- package/src/cli/load-runner.js +121 -0
- package/src/cli/report-server.js +723 -0
- package/src/cli/report.js +116 -0
- package/src/cli/scaffold.archive.js +1371 -0
- package/src/cli/scaffold.js +349 -0
- package/src/cli/start.js +74 -0
- package/src/html.js +19 -0
- package/src/mcp/server.js +884 -0
- package/src/mcp/validate-worker.js +110 -0
- package/src/runtime/image.js +74 -0
- package/src/runtime/image.test.js +111 -0
- package/src/runtime/index.js +621 -0
- package/src/runtime/navigate.js +146 -0
- package/src/runtime/runtime.test.js +773 -0
- package/src/runtime/ssr.js +464 -0
- package/src/runtime/ssr.test.js +421 -0
- package/src/runtime/store.js +92 -0
- package/src/runtime/toast.js +163 -0
- package/src/server/index.js +1386 -0
- package/src/server/server.test.js +1248 -0
- package/src/spec/schema.js +428 -0
- package/src/spec/schema.test.js +291 -0
- package/src/store/index.js +102 -0
- package/src/store/store.test.js +210 -0
- package/src/testing/html.js +283 -0
- package/src/testing/index.js +249 -0
- package/src/testing/testing.test.js +450 -0
- package/src/ui/accordion.js +28 -0
- package/src/ui/alert.js +43 -0
- package/src/ui/app-badge.js +48 -0
- package/src/ui/avatar.js +47 -0
- package/src/ui/badge.js +24 -0
- package/src/ui/banner.js +26 -0
- package/src/ui/breadcrumbs.js +38 -0
- package/src/ui/button.js +66 -0
- package/src/ui/card.js +34 -0
- package/src/ui/carousel.js +59 -0
- package/src/ui/charts.js +321 -0
- package/src/ui/checkbox.js +65 -0
- package/src/ui/cluster.js +44 -0
- package/src/ui/code-window.js +39 -0
- package/src/ui/container.js +24 -0
- package/src/ui/cta.js +37 -0
- package/src/ui/divider.js +29 -0
- package/src/ui/empty.js +33 -0
- package/src/ui/feature.js +33 -0
- package/src/ui/fieldset.js +37 -0
- package/src/ui/fileupload.js +89 -0
- package/src/ui/footer.js +38 -0
- package/src/ui/grid.js +36 -0
- package/src/ui/heading.js +45 -0
- package/src/ui/hero.js +37 -0
- package/src/ui/icons.js +161 -0
- package/src/ui/index.js +89 -0
- package/src/ui/input.js +74 -0
- package/src/ui/list.js +36 -0
- package/src/ui/media.js +44 -0
- package/src/ui/modal.js +80 -0
- package/src/ui/nav.js +61 -0
- package/src/ui/pricing.js +56 -0
- package/src/ui/progress.js +62 -0
- package/src/ui/prose.js +29 -0
- package/src/ui/pullquote.js +34 -0
- package/src/ui/radio.js +102 -0
- package/src/ui/rating.js +93 -0
- package/src/ui/search.js +77 -0
- package/src/ui/section.js +69 -0
- package/src/ui/segmented.js +50 -0
- package/src/ui/select.js +77 -0
- package/src/ui/slider.js +84 -0
- package/src/ui/spinner.js +34 -0
- package/src/ui/stack.js +36 -0
- package/src/ui/stat.js +52 -0
- package/src/ui/stepper.js +46 -0
- package/src/ui/switch.js +57 -0
- package/src/ui/table.js +45 -0
- package/src/ui/testimonial.js +48 -0
- package/src/ui/textarea.js +72 -0
- package/src/ui/timeline.js +72 -0
- package/src/ui/tooltip.js +28 -0
- package/src/ui/ui.test.js +1241 -0
- package/src/ui/uiimage.js +65 -0
- package/tsconfig.json +13 -0
- package/types/html.d.ts +17 -0
- package/types/image.d.ts +70 -0
- package/types/index.d.ts +7 -0
- package/types/navigate.d.ts +38 -0
- package/types/runtime.d.ts +63 -0
- package/types/schema.d.ts +243 -0
- package/types/server.d.ts +145 -0
- package/types/ssr.d.ts +110 -0
- package/types/testing.d.ts +154 -0
- package/types/ui.d.ts +704 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Breadcrumbs
|
|
3
|
+
*
|
|
4
|
+
* Accessible breadcrumb navigation. The last item (current page) renders as a
|
|
5
|
+
* <span> with aria-current="page". All other items render as links.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {Array} opts.items - Array of { label, href }. Last item has no href.
|
|
9
|
+
* @param {string} opts.separator - Separator character (default: '/')
|
|
10
|
+
* @param {string} opts.class
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { escHtml as e } from '../html.js'
|
|
14
|
+
|
|
15
|
+
export function breadcrumbs({
|
|
16
|
+
items = [],
|
|
17
|
+
separator = '/',
|
|
18
|
+
class: cls = '',
|
|
19
|
+
} = {}) {
|
|
20
|
+
const navClasses = ['ui-breadcrumbs', cls].filter(Boolean).join(' ')
|
|
21
|
+
|
|
22
|
+
const listItems = items.map((item, i) => {
|
|
23
|
+
const isLast = i === items.length - 1
|
|
24
|
+
const sep = i > 0
|
|
25
|
+
? `<span class="ui-breadcrumbs-sep" aria-hidden="true">${e(separator)}</span>`
|
|
26
|
+
: ''
|
|
27
|
+
|
|
28
|
+
const content = isLast
|
|
29
|
+
? `<span class="ui-breadcrumbs-current" aria-current="page">${e(item.label)}</span>`
|
|
30
|
+
: `<a href="${e(item.href)}" class="ui-breadcrumbs-link">${e(item.label)}</a>`
|
|
31
|
+
|
|
32
|
+
return `<li class="ui-breadcrumbs-item">${sep}${content}</li>`
|
|
33
|
+
}).join('')
|
|
34
|
+
|
|
35
|
+
return `<nav aria-label="Breadcrumb" class="${e(navClasses)}">
|
|
36
|
+
<ol class="ui-breadcrumbs-list">${listItems}</ol>
|
|
37
|
+
</nav>`
|
|
38
|
+
}
|
package/src/ui/button.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Button
|
|
3
|
+
*
|
|
4
|
+
* Renders as <a> when href is provided, <button> otherwise.
|
|
5
|
+
* All visual variation goes through CSS modifier classes — never inline styles.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {string} opts.label - Visible text (required)
|
|
9
|
+
* @param {'primary'|'secondary'|'ghost'|'danger'} opts.variant
|
|
10
|
+
* @param {'sm'|'md'|'lg'} opts.size
|
|
11
|
+
* @param {string} opts.href - Renders as <a> when set
|
|
12
|
+
* @param {boolean} opts.disabled
|
|
13
|
+
* @param {'button'|'submit'|'reset'} opts.type
|
|
14
|
+
* @param {string} opts.icon - SVG HTML prepended inside the element
|
|
15
|
+
* @param {string} opts.iconAfter - SVG HTML appended inside the element
|
|
16
|
+
* @param {boolean} opts.fullWidth
|
|
17
|
+
* @param {string} opts.class - Extra CSS classes appended to the element
|
|
18
|
+
* @param {object} opts.attrs - Extra HTML attributes (key→value) for <button> only
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { escHtml as e } from '../html.js'
|
|
22
|
+
|
|
23
|
+
const VARIANTS = new Set(['primary', 'secondary', 'ghost', 'danger'])
|
|
24
|
+
const SIZES = new Set(['sm', 'md', 'lg'])
|
|
25
|
+
|
|
26
|
+
export function button({
|
|
27
|
+
label = '',
|
|
28
|
+
variant = 'primary',
|
|
29
|
+
size = 'md',
|
|
30
|
+
href,
|
|
31
|
+
disabled = false,
|
|
32
|
+
type = 'button',
|
|
33
|
+
icon = '',
|
|
34
|
+
iconAfter = '',
|
|
35
|
+
fullWidth = false,
|
|
36
|
+
class: cls = '',
|
|
37
|
+
attrs = {},
|
|
38
|
+
} = {}) {
|
|
39
|
+
if (!VARIANTS.has(variant)) variant = 'primary'
|
|
40
|
+
if (!SIZES.has(size)) size = 'md'
|
|
41
|
+
|
|
42
|
+
const classes = [
|
|
43
|
+
'ui-btn',
|
|
44
|
+
`ui-btn--${variant}`,
|
|
45
|
+
`ui-btn--${size}`,
|
|
46
|
+
fullWidth ? 'ui-btn--full' : '',
|
|
47
|
+
disabled ? 'ui-btn--disabled' : '',
|
|
48
|
+
cls,
|
|
49
|
+
].filter(Boolean).join(' ')
|
|
50
|
+
|
|
51
|
+
const inner = [
|
|
52
|
+
icon ? `<span class="ui-btn-icon" aria-hidden="true">${icon}</span>` : '',
|
|
53
|
+
`<span>${e(label)}</span>`,
|
|
54
|
+
iconAfter ? `<span class="ui-btn-icon ui-btn-icon--after" aria-hidden="true">${iconAfter}</span>` : '',
|
|
55
|
+
].join('')
|
|
56
|
+
|
|
57
|
+
if (href) {
|
|
58
|
+
return `<a href="${e(href)}" class="${e(classes)}"${disabled ? ' aria-disabled="true" tabindex="-1"' : ''}>${inner}</a>`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const attrsStr = Object.entries(attrs)
|
|
62
|
+
.map(([k, v]) => ` ${e(k)}="${e(String(v))}"`)
|
|
63
|
+
.join('')
|
|
64
|
+
|
|
65
|
+
return `<button type="${e(type)}" class="${e(classes)}"${disabled ? ' disabled aria-disabled="true"' : ''}${attrsStr}>${inner}</button>`
|
|
66
|
+
}
|
package/src/ui/card.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Card
|
|
3
|
+
*
|
|
4
|
+
* Surface container with optional title and footer.
|
|
5
|
+
* Pass HTML strings for content and footer — they are not escaped.
|
|
6
|
+
* Escape user data before passing it in.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} opts
|
|
9
|
+
* @param {string} opts.title - Escaped heading text (optional)
|
|
10
|
+
* @param {number} opts.level - Heading level 1–6 (default 3). Controls the tag; visual style is always ui-card-title.
|
|
11
|
+
* @param {string} opts.content - HTML string for the card body
|
|
12
|
+
* @param {string} opts.footer - HTML string for the card footer (optional)
|
|
13
|
+
* @param {boolean} opts.flush - Remove internal padding (for full-bleed content)
|
|
14
|
+
* @param {string} opts.class
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { escHtml as e } from '../html.js'
|
|
18
|
+
|
|
19
|
+
export function card({
|
|
20
|
+
title = '',
|
|
21
|
+
level = 3,
|
|
22
|
+
content = '',
|
|
23
|
+
footer = '',
|
|
24
|
+
flush = false,
|
|
25
|
+
class: cls = '',
|
|
26
|
+
} = {}) {
|
|
27
|
+
const classes = ['ui-card', flush ? 'ui-card--flush' : '', cls].filter(Boolean).join(' ')
|
|
28
|
+
const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
|
|
29
|
+
|
|
30
|
+
const titleHtml = title ? `<div class="ui-card-header"><${tag} class="ui-card-title">${e(title)}</${tag}></div>` : ''
|
|
31
|
+
const footerHtml = footer ? `<div class="ui-card-footer">${footer}</div>` : ''
|
|
32
|
+
|
|
33
|
+
return `<div class="${e(classes)}">${titleHtml}<div class="ui-card-body">${content}</div>${footerHtml}</div>`
|
|
34
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Carousel / Slider
|
|
3
|
+
*
|
|
4
|
+
* CSS scroll-snap carousel with optional prev/next arrows and dot navigation.
|
|
5
|
+
* Requires pulse-ui.js for button and dot interactivity.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {string[]} opts.slides - Array of raw HTML strings — one per slide
|
|
9
|
+
* @param {boolean} opts.arrows - Show prev/next arrow buttons (default: true)
|
|
10
|
+
* @param {boolean} opts.dots - Show dot navigation (default: true)
|
|
11
|
+
* @param {string} opts.class
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { escHtml as e } from '../html.js'
|
|
15
|
+
import { iconChevronLeft, iconChevronRight } from './icons.js'
|
|
16
|
+
|
|
17
|
+
let _carouselId = 0
|
|
18
|
+
|
|
19
|
+
export function carousel({
|
|
20
|
+
slides = [],
|
|
21
|
+
arrows = true,
|
|
22
|
+
dots = true,
|
|
23
|
+
class: cls = '',
|
|
24
|
+
} = {}) {
|
|
25
|
+
const id = `carousel-${++_carouselId}`
|
|
26
|
+
const classes = ['ui-carousel', cls].filter(Boolean).join(' ')
|
|
27
|
+
|
|
28
|
+
const slidesHtml = slides
|
|
29
|
+
.map((s, i) => {
|
|
30
|
+
const panelId = `${id}-panel-${i + 1}`
|
|
31
|
+
const tabId = `${id}-tab-${i + 1}`
|
|
32
|
+
return `<div class="ui-carousel-slide" id="${panelId}" role="tabpanel" aria-labelledby="${tabId}" tabindex="0">${s}</div>`
|
|
33
|
+
})
|
|
34
|
+
.join('\n ')
|
|
35
|
+
|
|
36
|
+
const arrowsHtml = arrows ? `
|
|
37
|
+
<button class="ui-carousel-btn ui-carousel-prev" type="button" aria-label="Previous slide" hidden>
|
|
38
|
+
${iconChevronLeft({ size: 16 })}
|
|
39
|
+
</button>
|
|
40
|
+
<button class="ui-carousel-btn ui-carousel-next" type="button" aria-label="Next slide"${slides.length <= 1 ? ' hidden' : ''}>
|
|
41
|
+
${iconChevronRight({ size: 16 })}
|
|
42
|
+
</button>` : ''
|
|
43
|
+
|
|
44
|
+
const dotsHtml = dots && slides.length > 1 ? `
|
|
45
|
+
<div class="ui-carousel-dots" role="tablist" aria-label="Slides">
|
|
46
|
+
${slides.map((_, i) => {
|
|
47
|
+
const tabId = `${id}-tab-${i + 1}`
|
|
48
|
+
const panelId = `${id}-panel-${i + 1}`
|
|
49
|
+
const active = i === 0
|
|
50
|
+
return `<button class="ui-carousel-dot${active ? ' active' : ''}" id="${tabId}" type="button" role="tab" aria-selected="${active}" aria-controls="${panelId}" tabindex="${active ? '0' : '-1'}" aria-label="Slide ${i + 1}"></button>`
|
|
51
|
+
}).join('\n ')}
|
|
52
|
+
</div>` : ''
|
|
53
|
+
|
|
54
|
+
return `<div class="${e(classes)}">
|
|
55
|
+
<div class="ui-carousel-track">
|
|
56
|
+
${slidesHtml}
|
|
57
|
+
</div>${arrowsHtml}${dotsHtml}
|
|
58
|
+
</div>`
|
|
59
|
+
}
|
package/src/ui/charts.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Charts
|
|
3
|
+
*
|
|
4
|
+
* Server-rendered SVG charts. Pure functions, no client JS, no dependencies.
|
|
5
|
+
* All charts use CSS custom properties for colours and scale to 100% width
|
|
6
|
+
* (except sparkline and donutChart which have explicit dimensions).
|
|
7
|
+
*
|
|
8
|
+
* @exports barChart - Vertical bar chart
|
|
9
|
+
* @exports lineChart - Line chart with optional area fill
|
|
10
|
+
* @exports donutChart - Donut / pie chart
|
|
11
|
+
* @exports sparkline - Minimal inline line for stat tiles
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { escHtml as e } from '../html.js'
|
|
15
|
+
|
|
16
|
+
// ─── Internal constants ───────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const IW = 500 // internal viewBox width for bar / line charts
|
|
19
|
+
|
|
20
|
+
const COLOR_MAP = {
|
|
21
|
+
accent: 'var(--ui-accent)',
|
|
22
|
+
success: 'var(--ui-green)',
|
|
23
|
+
warning: 'var(--ui-yellow)',
|
|
24
|
+
error: 'var(--ui-red)',
|
|
25
|
+
blue: 'var(--ui-blue)',
|
|
26
|
+
muted: 'var(--ui-muted)',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PALETTE = ['accent', 'blue', 'success', 'warning', 'error', 'muted']
|
|
30
|
+
|
|
31
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function col(c) { return COLOR_MAP[c] ?? COLOR_MAP.accent }
|
|
34
|
+
function r1(n) { return Math.round(n * 10) / 10 } // 1 decimal place
|
|
35
|
+
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)) }
|
|
36
|
+
|
|
37
|
+
function fmt(n) {
|
|
38
|
+
const abs = Math.abs(n)
|
|
39
|
+
if (abs >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'
|
|
40
|
+
if (abs >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'k'
|
|
41
|
+
return String(Math.round(n))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Bar chart ────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Vertical bar chart.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} opts
|
|
50
|
+
* @param {Array<{label:string, value:number}>} opts.data
|
|
51
|
+
* @param {number} opts.height - SVG height in px (default: 220)
|
|
52
|
+
* @param {'accent'|'success'|'warning'|'error'|'blue'|'muted'} opts.color
|
|
53
|
+
* @param {boolean} opts.showValues - Show value labels above each bar
|
|
54
|
+
* @param {boolean} opts.showGrid - Show horizontal grid lines (default: true)
|
|
55
|
+
* @param {number} opts.gap - Gap between bars as fraction 0–0.9 (default: 0.25)
|
|
56
|
+
* @param {string} opts.class
|
|
57
|
+
*/
|
|
58
|
+
export function barChart({
|
|
59
|
+
data = [],
|
|
60
|
+
height = 220,
|
|
61
|
+
color = 'accent',
|
|
62
|
+
showValues = false,
|
|
63
|
+
showGrid = true,
|
|
64
|
+
gap = 0.25,
|
|
65
|
+
class: cls = '',
|
|
66
|
+
} = {}) {
|
|
67
|
+
if (!data.length) return ''
|
|
68
|
+
|
|
69
|
+
const pad = { top: showValues ? 28 : 16, right: 16, bottom: 40, left: 44 }
|
|
70
|
+
const plotW = IW - pad.left - pad.right
|
|
71
|
+
const plotH = height - pad.top - pad.bottom
|
|
72
|
+
|
|
73
|
+
const values = data.map(d => Number(d.value) || 0)
|
|
74
|
+
const maxVal = Math.max(...values, 0)
|
|
75
|
+
const minVal = Math.min(...values, 0)
|
|
76
|
+
const range = maxVal - minVal || 1
|
|
77
|
+
const c = col(color)
|
|
78
|
+
|
|
79
|
+
const yFor = v => r1(pad.top + plotH - ((v - minVal) / range) * plotH)
|
|
80
|
+
const zeroY = yFor(0)
|
|
81
|
+
|
|
82
|
+
// Grid lines + Y-axis labels
|
|
83
|
+
let grid = ''
|
|
84
|
+
if (showGrid) {
|
|
85
|
+
for (let i = 0; i <= 4; i++) {
|
|
86
|
+
const v = minVal + (i / 4) * range
|
|
87
|
+
const y = yFor(v)
|
|
88
|
+
grid += `<line x1="${pad.left}" y1="${r1(y)}" x2="${IW - pad.right}" y2="${r1(y)}" stroke="var(--ui-border)" stroke-width="1"/>`
|
|
89
|
+
grid += `<text x="${pad.left - 6}" y="${r1(y + 4)}" text-anchor="end" font-size="11" fill="var(--ui-muted)" font-family="var(--ui-font)">${e(fmt(v))}</text>`
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Zero baseline
|
|
94
|
+
const baseline = `<line x1="${pad.left}" y1="${zeroY}" x2="${IW - pad.right}" y2="${zeroY}" stroke="var(--ui-border)" stroke-width="1.5"/>`
|
|
95
|
+
|
|
96
|
+
// Bars + labels
|
|
97
|
+
const slotW = plotW / data.length
|
|
98
|
+
const barW = r1(slotW * (1 - clamp(gap, 0.05, 0.9)))
|
|
99
|
+
|
|
100
|
+
const bars = data.map((d, i) => {
|
|
101
|
+
const v = Number(d.value) || 0
|
|
102
|
+
const bx = r1(pad.left + i * slotW + (slotW - barW) / 2)
|
|
103
|
+
const by = r1(Math.min(yFor(v), zeroY))
|
|
104
|
+
const bh = r1(Math.max(Math.abs(yFor(v) - zeroY), 1))
|
|
105
|
+
const mx = r1(bx + barW / 2)
|
|
106
|
+
|
|
107
|
+
let out = `<rect x="${bx}" y="${by}" width="${barW}" height="${bh}" fill="${c}" rx="2"/>`
|
|
108
|
+
if (showValues) {
|
|
109
|
+
out += `<text x="${mx}" y="${r1(by - 5)}" text-anchor="middle" font-size="11" font-weight="600" fill="${c}" font-family="var(--ui-font)">${e(fmt(v))}</text>`
|
|
110
|
+
}
|
|
111
|
+
if (d.label != null) {
|
|
112
|
+
out += `<text x="${mx}" y="${height - 8}" text-anchor="middle" font-size="11" fill="var(--ui-muted)" font-family="var(--ui-font)">${e(String(d.label))}</text>`
|
|
113
|
+
}
|
|
114
|
+
return out
|
|
115
|
+
}).join('')
|
|
116
|
+
|
|
117
|
+
const ariaLabel = `Bar chart: ${data.map(d => `${d.label ?? ''} ${d.value}`).join(', ')}`
|
|
118
|
+
const clsAttr = cls ? ` class="${e(cls)}"` : ''
|
|
119
|
+
|
|
120
|
+
return `<svg${clsAttr} viewBox="0 0 ${IW} ${height}" width="100%" height="${height}" role="img" aria-label="${e(ariaLabel)}">${grid}${baseline}${bars}</svg>`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Line chart ───────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Line chart with optional area fill.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} opts
|
|
129
|
+
* @param {Array<{label:string, value:number}>} opts.data
|
|
130
|
+
* @param {number} opts.height - SVG height in px (default: 220)
|
|
131
|
+
* @param {'accent'|'success'|'warning'|'error'|'blue'|'muted'} opts.color
|
|
132
|
+
* @param {boolean} opts.area - Fill the area under the line
|
|
133
|
+
* @param {boolean} opts.showDots - Show dots at each data point (default: true)
|
|
134
|
+
* @param {boolean} opts.showGrid - Show horizontal grid lines (default: true)
|
|
135
|
+
* @param {string} opts.class
|
|
136
|
+
*/
|
|
137
|
+
export function lineChart({
|
|
138
|
+
data = [],
|
|
139
|
+
height = 220,
|
|
140
|
+
color = 'accent',
|
|
141
|
+
area = false,
|
|
142
|
+
showDots = true,
|
|
143
|
+
showGrid = true,
|
|
144
|
+
class: cls = '',
|
|
145
|
+
} = {}) {
|
|
146
|
+
if (data.length < 2) return ''
|
|
147
|
+
|
|
148
|
+
const pad = { top: 16, right: 16, bottom: 40, left: 44 }
|
|
149
|
+
const plotW = IW - pad.left - pad.right
|
|
150
|
+
const plotH = height - pad.top - pad.bottom
|
|
151
|
+
|
|
152
|
+
const values = data.map(d => Number(d.value) || 0)
|
|
153
|
+
const maxVal = Math.max(...values)
|
|
154
|
+
const minVal = Math.min(...values)
|
|
155
|
+
const range = maxVal - minVal || 1
|
|
156
|
+
const c = col(color)
|
|
157
|
+
|
|
158
|
+
const xFor = i => r1(pad.left + (i / (data.length - 1)) * plotW)
|
|
159
|
+
const yFor = v => r1(pad.top + plotH - ((v - minVal) / range) * plotH)
|
|
160
|
+
|
|
161
|
+
// Grid lines + Y-axis labels
|
|
162
|
+
let grid = ''
|
|
163
|
+
if (showGrid) {
|
|
164
|
+
for (let i = 0; i <= 4; i++) {
|
|
165
|
+
const v = minVal + (i / 4) * range
|
|
166
|
+
const y = yFor(v)
|
|
167
|
+
grid += `<line x1="${pad.left}" y1="${r1(y)}" x2="${IW - pad.right}" y2="${r1(y)}" stroke="var(--ui-border)" stroke-width="1"/>`
|
|
168
|
+
grid += `<text x="${pad.left - 6}" y="${r1(y + 4)}" text-anchor="end" font-size="11" fill="var(--ui-muted)" font-family="var(--ui-font)">${e(fmt(v))}</text>`
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const pts = data.map((d, i) => [xFor(i), yFor(Number(d.value) || 0)])
|
|
173
|
+
const points = pts.map(([x, y]) => `${x},${y}`).join(' ')
|
|
174
|
+
|
|
175
|
+
// Area fill
|
|
176
|
+
let areaPath = ''
|
|
177
|
+
if (area) {
|
|
178
|
+
const bottom = r1(pad.top + plotH)
|
|
179
|
+
const d = `M${pts[0][0]},${bottom} ` + pts.map(([x, y]) => `L${x},${y}`).join(' ') + ` L${pts.at(-1)[0]},${bottom} Z`
|
|
180
|
+
areaPath = `<path d="${d}" fill="${c}" fill-opacity="0.12"/>`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const line = `<polyline points="${points}" fill="none" stroke="${c}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`
|
|
184
|
+
const dots = showDots ? pts.map(([x, y]) => `<circle cx="${x}" cy="${y}" r="3.5" fill="${c}"/>`).join('') : ''
|
|
185
|
+
|
|
186
|
+
// X-axis labels
|
|
187
|
+
const xlabels = data.map((d, i) =>
|
|
188
|
+
d.label != null
|
|
189
|
+
? `<text x="${xFor(i)}" y="${height - 8}" text-anchor="middle" font-size="11" fill="var(--ui-muted)" font-family="var(--ui-font)">${e(String(d.label))}</text>`
|
|
190
|
+
: ''
|
|
191
|
+
).join('')
|
|
192
|
+
|
|
193
|
+
const ariaLabel = `Line chart: ${data.map(d => `${d.label ?? ''} ${d.value}`).join(', ')}`
|
|
194
|
+
const clsAttr = cls ? ` class="${e(cls)}"` : ''
|
|
195
|
+
|
|
196
|
+
return `<svg${clsAttr} viewBox="0 0 ${IW} ${height}" width="100%" height="${height}" role="img" aria-label="${e(ariaLabel)}">${grid}${areaPath}${line}${dots}${xlabels}</svg>`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Donut chart ──────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Donut (ring) chart. Each segment can have its own colour.
|
|
203
|
+
*
|
|
204
|
+
* @param {object} opts
|
|
205
|
+
* @param {Array<{label:string, value:number, color?:string}>} opts.data
|
|
206
|
+
* @param {number} opts.size - Diameter in px (default: 200)
|
|
207
|
+
* @param {number} opts.thickness - Ring thickness in px (default: 40)
|
|
208
|
+
* @param {string} opts.label - Large text in the centre
|
|
209
|
+
* @param {string} opts.sublabel - Smaller text below the centre label
|
|
210
|
+
* @param {string} opts.class
|
|
211
|
+
*/
|
|
212
|
+
export function donutChart({
|
|
213
|
+
data = [],
|
|
214
|
+
size = 200,
|
|
215
|
+
thickness = 40,
|
|
216
|
+
label = '',
|
|
217
|
+
sublabel = '',
|
|
218
|
+
class: cls = '',
|
|
219
|
+
} = {}) {
|
|
220
|
+
if (!data.length) return ''
|
|
221
|
+
|
|
222
|
+
const cx = size / 2
|
|
223
|
+
const cy = size / 2
|
|
224
|
+
const r = cx - 8
|
|
225
|
+
const innerR = r - clamp(thickness, 4, r - 4)
|
|
226
|
+
const total = data.reduce((s, d) => s + Math.max(0, Number(d.value) || 0), 0)
|
|
227
|
+
if (!total) return ''
|
|
228
|
+
|
|
229
|
+
let angle = -Math.PI / 2 // start at 12 o'clock
|
|
230
|
+
|
|
231
|
+
const segments = data.map((d, i) => {
|
|
232
|
+
const v = Math.max(0, Number(d.value) || 0)
|
|
233
|
+
const frac = v / total
|
|
234
|
+
const sweep = frac * 2 * Math.PI
|
|
235
|
+
const start = angle
|
|
236
|
+
const end = angle + sweep
|
|
237
|
+
angle = end
|
|
238
|
+
|
|
239
|
+
const c = col(d.color || PALETTE[i % PALETTE.length])
|
|
240
|
+
|
|
241
|
+
// Full circle edge case
|
|
242
|
+
if (frac >= 1 - 1e-10) {
|
|
243
|
+
const mid = r1((r + innerR) / 2)
|
|
244
|
+
return `<circle cx="${cx}" cy="${cy}" r="${mid}" fill="none" stroke="${c}" stroke-width="${thickness}"/>`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const x1 = r1(cx + r * Math.cos(start))
|
|
248
|
+
const y1 = r1(cy + r * Math.sin(start))
|
|
249
|
+
const x2 = r1(cx + r * Math.cos(end))
|
|
250
|
+
const y2 = r1(cy + r * Math.sin(end))
|
|
251
|
+
const x3 = r1(cx + innerR * Math.cos(end))
|
|
252
|
+
const y3 = r1(cy + innerR * Math.sin(end))
|
|
253
|
+
const x4 = r1(cx + innerR * Math.cos(start))
|
|
254
|
+
const y4 = r1(cy + innerR * Math.sin(start))
|
|
255
|
+
const lg = sweep > Math.PI ? 1 : 0
|
|
256
|
+
|
|
257
|
+
const path = `M${x1},${y1} A${r},${r} 0 ${lg} 1 ${x2},${y2} L${x3},${y3} A${innerR},${innerR} 0 ${lg} 0 ${x4},${y4} Z`
|
|
258
|
+
return `<path d="${path}" fill="${c}"/>`
|
|
259
|
+
}).join('')
|
|
260
|
+
|
|
261
|
+
// Centre text
|
|
262
|
+
const hasTwo = label && sublabel
|
|
263
|
+
const labelY = r1(cy + (hasTwo ? -6 : 6))
|
|
264
|
+
const subY = r1(cy + 16)
|
|
265
|
+
const labelEl = label ? `<text x="${cx}" y="${labelY}" text-anchor="middle" font-size="${thickness > 30 ? 22 : 16}" font-weight="700" fill="var(--ui-text)" font-family="var(--ui-font)">${e(label)}</text>` : ''
|
|
266
|
+
const subEl = sublabel ? `<text x="${cx}" y="${subY}" text-anchor="middle" font-size="12" fill="var(--ui-muted)" font-family="var(--ui-font)">${e(sublabel)}</text>` : ''
|
|
267
|
+
|
|
268
|
+
const ariaLabel = `Donut chart: ${data.map(d => `${d.label ?? ''} ${d.value}`).join(', ')}`
|
|
269
|
+
const clsAttr = cls ? ` class="${e(cls)}"` : ''
|
|
270
|
+
|
|
271
|
+
return `<svg${clsAttr} viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" role="img" aria-label="${e(ariaLabel)}">${segments}${labelEl}${subEl}</svg>`
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Sparkline ────────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Minimal inline line chart for use inside stat tiles or table cells.
|
|
278
|
+
* Accepts a plain array of numbers.
|
|
279
|
+
*
|
|
280
|
+
* @param {object} opts
|
|
281
|
+
* @param {number[]} opts.data - Array of numeric values
|
|
282
|
+
* @param {number} opts.width - SVG width in px (default: 80)
|
|
283
|
+
* @param {number} opts.height - SVG height in px (default: 32)
|
|
284
|
+
* @param {'accent'|'success'|'warning'|'error'|'blue'|'muted'} opts.color
|
|
285
|
+
* @param {boolean} opts.area - Fill area under the line
|
|
286
|
+
* @param {string} opts.class
|
|
287
|
+
*/
|
|
288
|
+
export function sparkline({
|
|
289
|
+
data = [],
|
|
290
|
+
width = 80,
|
|
291
|
+
height = 32,
|
|
292
|
+
color = 'accent',
|
|
293
|
+
area = false,
|
|
294
|
+
class: cls = '',
|
|
295
|
+
} = {}) {
|
|
296
|
+
if (data.length < 2) return ''
|
|
297
|
+
|
|
298
|
+
const values = data.map(v => Number(v) || 0)
|
|
299
|
+
const min = Math.min(...values)
|
|
300
|
+
const max = Math.max(...values)
|
|
301
|
+
const range = max - min || 1
|
|
302
|
+
const pad = 2
|
|
303
|
+
const c = col(color)
|
|
304
|
+
|
|
305
|
+
const xFor = i => r1(i * (width / (data.length - 1)))
|
|
306
|
+
const yFor = v => r1(height - pad - ((v - min) / range) * (height - pad * 2))
|
|
307
|
+
|
|
308
|
+
const pts = values.map((v, i) => [xFor(i), yFor(v)])
|
|
309
|
+
const points = pts.map(([x, y]) => `${x},${y}`).join(' ')
|
|
310
|
+
|
|
311
|
+
let areaPath = ''
|
|
312
|
+
if (area) {
|
|
313
|
+
const bottom = height - pad
|
|
314
|
+
const d = `M${pts[0][0]},${bottom} ` + pts.map(([x, y]) => `L${x},${y}`).join(' ') + ` L${pts.at(-1)[0]},${bottom} Z`
|
|
315
|
+
areaPath = `<path d="${d}" fill="${c}" fill-opacity="0.15"/>`
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const clsAttr = cls ? ` class="${e(cls)}"` : ''
|
|
319
|
+
|
|
320
|
+
return `<svg${clsAttr} viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" aria-hidden="true">${areaPath}<polyline points="${points}" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`
|
|
321
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Checkbox
|
|
3
|
+
*
|
|
4
|
+
* Styled checkbox with optional label. Wraps a visually-hidden <input> and a
|
|
5
|
+
* custom box element driven by CSS :checked state.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {string} opts.name - Field name
|
|
9
|
+
* @param {string} opts.value - Submitted value
|
|
10
|
+
* @param {string} opts.label - Visible label text (escaped)
|
|
11
|
+
* @param {string} opts.labelHtml - Raw HTML label slot (not escaped — use for styled spans)
|
|
12
|
+
* @param {boolean} opts.checked
|
|
13
|
+
* @param {boolean} opts.disabled
|
|
14
|
+
* @param {string} opts.id - Override generated id
|
|
15
|
+
* @param {string} opts.event - data-event binding (e.g. 'change:toggle')
|
|
16
|
+
* @param {string} opts.hint - Helper text below the label
|
|
17
|
+
* @param {string} opts.error - Validation error message
|
|
18
|
+
* @param {string} opts.class
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { escHtml as e } from '../html.js'
|
|
22
|
+
|
|
23
|
+
export function checkbox({
|
|
24
|
+
name = '',
|
|
25
|
+
value = '',
|
|
26
|
+
label = '',
|
|
27
|
+
labelHtml = '',
|
|
28
|
+
checked = false,
|
|
29
|
+
disabled = false,
|
|
30
|
+
id = '',
|
|
31
|
+
event = '',
|
|
32
|
+
hint = '',
|
|
33
|
+
error = '',
|
|
34
|
+
class: cls = '',
|
|
35
|
+
} = {}) {
|
|
36
|
+
const uid = e(id || ['checkbox', name, value].filter(Boolean).join('-'))
|
|
37
|
+
|
|
38
|
+
const classes = [
|
|
39
|
+
'ui-checkbox',
|
|
40
|
+
disabled ? 'ui-checkbox--disabled' : '',
|
|
41
|
+
error ? 'ui-checkbox--error' : '',
|
|
42
|
+
cls,
|
|
43
|
+
].filter(Boolean).join(' ')
|
|
44
|
+
|
|
45
|
+
const labelContent = labelHtml
|
|
46
|
+
? labelHtml
|
|
47
|
+
: label ? `<span class="ui-checkbox-label">${e(label)}</span>` : ''
|
|
48
|
+
|
|
49
|
+
return `<label class="${e(classes)}">
|
|
50
|
+
<input
|
|
51
|
+
type="checkbox"
|
|
52
|
+
id="${uid}"
|
|
53
|
+
${name ? `name="${e(name)}"` : ''}
|
|
54
|
+
${value ? `value="${e(value)}"` : ''}
|
|
55
|
+
class="ui-checkbox-input"
|
|
56
|
+
${checked ? 'checked' : ''}
|
|
57
|
+
${disabled ? 'disabled' : ''}
|
|
58
|
+
${event ? `data-event="${e(event)}"` : ''}
|
|
59
|
+
>
|
|
60
|
+
<span class="ui-checkbox-box" aria-hidden="true"></span>
|
|
61
|
+
${labelContent}
|
|
62
|
+
${hint ? `<p class="ui-hint">${e(hint)}</p>` : ''}
|
|
63
|
+
${error ? `<p class="ui-error" role="alert">${e(error)}</p>` : ''}
|
|
64
|
+
</label>`
|
|
65
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Cluster
|
|
3
|
+
*
|
|
4
|
+
* Flex row with wrapping. Groups inline elements horizontally
|
|
5
|
+
* with consistent gap — buttons, badges, app store badges, stat rows, etc.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {string} opts.content - Raw HTML slot
|
|
9
|
+
* @param {'xs'|'sm'|'md'|'lg'} opts.gap - Gap between children (default: 'md')
|
|
10
|
+
* @param {'start'|'center'|'end'|'between'} opts.justify - justify-content (default: 'start')
|
|
11
|
+
* @param {'start'|'center'|'end'} opts.align - align-items (default: 'center')
|
|
12
|
+
* @param {boolean} opts.wrap - Allow wrapping (default: true)
|
|
13
|
+
* @param {string} opts.class
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { escHtml as e } from '../html.js'
|
|
17
|
+
|
|
18
|
+
const GAPS = new Set(['xs', 'sm', 'md', 'lg'])
|
|
19
|
+
const JUSTIFYS = new Set(['start', 'center', 'end', 'between'])
|
|
20
|
+
const ALIGNS = new Set(['start', 'center', 'end'])
|
|
21
|
+
|
|
22
|
+
export function cluster({
|
|
23
|
+
content = '',
|
|
24
|
+
gap = 'md',
|
|
25
|
+
justify = 'start',
|
|
26
|
+
align = 'center',
|
|
27
|
+
wrap = true,
|
|
28
|
+
class: cls = '',
|
|
29
|
+
} = {}) {
|
|
30
|
+
if (!GAPS.has(gap)) gap = 'md'
|
|
31
|
+
if (!JUSTIFYS.has(justify)) justify = 'start'
|
|
32
|
+
if (!ALIGNS.has(align)) align = 'center'
|
|
33
|
+
|
|
34
|
+
const classes = [
|
|
35
|
+
'ui-cluster',
|
|
36
|
+
gap !== 'md' && `ui-cluster--gap-${gap}`,
|
|
37
|
+
justify !== 'start' && `ui-cluster--justify-${justify}`,
|
|
38
|
+
align !== 'center' && `ui-cluster--align-${align}`,
|
|
39
|
+
!wrap && 'ui-cluster--nowrap',
|
|
40
|
+
cls,
|
|
41
|
+
].filter(Boolean).join(' ')
|
|
42
|
+
|
|
43
|
+
return `<div class="${e(classes)}">${content}</div>`
|
|
44
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Code Window
|
|
3
|
+
*
|
|
4
|
+
* A code block with macOS-style window chrome — three coloured dots and an
|
|
5
|
+
* optional filename label. The content slot accepts pre-highlighted HTML
|
|
6
|
+
* (spans with syntax-token classes) or plain text.
|
|
7
|
+
*
|
|
8
|
+
* Interactive syntax highlighting is out of scope — pass pre-rendered HTML
|
|
9
|
+
* or plain text. The component handles all the chrome, layout, scroll, and
|
|
10
|
+
* monospace typography.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {string} opts.content - Raw HTML slot — highlighted code HTML or plain text
|
|
14
|
+
* @param {string} opts.filename - Filename shown in the chrome bar (e.g. 'home.js')
|
|
15
|
+
* @param {string} opts.lang - Language label shown on the right of the chrome (e.g. 'JavaScript')
|
|
16
|
+
* @param {string} opts.class
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { escHtml as e } from '../html.js'
|
|
20
|
+
|
|
21
|
+
export function codeWindow({
|
|
22
|
+
content = '',
|
|
23
|
+
filename = '',
|
|
24
|
+
lang = '',
|
|
25
|
+
class: cls = '',
|
|
26
|
+
} = {}) {
|
|
27
|
+
const classes = ['ui-code-window', cls].filter(Boolean).join(' ')
|
|
28
|
+
|
|
29
|
+
return `<div class="${e(classes)}" role="region"${filename ? ` aria-label="${e(filename)}"` : ''}>
|
|
30
|
+
<div class="ui-code-window-chrome" aria-hidden="true">
|
|
31
|
+
<span class="ui-code-window-dot"></span>
|
|
32
|
+
<span class="ui-code-window-dot"></span>
|
|
33
|
+
<span class="ui-code-window-dot"></span>
|
|
34
|
+
${filename ? `<span class="ui-code-window-filename">${e(filename)}</span>` : ''}
|
|
35
|
+
${lang ? `<span class="ui-code-window-lang">${e(lang)}</span>` : ''}
|
|
36
|
+
</div>
|
|
37
|
+
<pre class="ui-code-window-pre"><code class="ui-code-window-code">${content}</code></pre>
|
|
38
|
+
</div>`
|
|
39
|
+
}
|