@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
package/src/ui/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Component library
|
|
3
|
+
*
|
|
4
|
+
* Server-rendered HTML string components. Import what you need:
|
|
5
|
+
*
|
|
6
|
+
* import { button, card, input } from '@invisibleloop/pulse/ui'
|
|
7
|
+
*
|
|
8
|
+
* All components:
|
|
9
|
+
* - Are pure functions: (props) => HTML string
|
|
10
|
+
* - Escape user data automatically
|
|
11
|
+
* - Use CSS custom properties for all visual values
|
|
12
|
+
* - Require no client-side JS
|
|
13
|
+
* - Meet WCAG AA contrast and keyboard accessibility requirements
|
|
14
|
+
* - Work at all viewport sizes
|
|
15
|
+
*
|
|
16
|
+
* Theming: override CSS custom properties in :root in your stylesheet.
|
|
17
|
+
* Extending: add new CSS modifier classes (e.g. .ui-btn--brand).
|
|
18
|
+
* Do not modify component source files — extend via CSS only.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export { button } from './button.js'
|
|
22
|
+
export { badge } from './badge.js'
|
|
23
|
+
export { card } from './card.js'
|
|
24
|
+
export { input } from './input.js'
|
|
25
|
+
export { search } from './search.js'
|
|
26
|
+
export { fieldset } from './fieldset.js'
|
|
27
|
+
export { select } from './select.js'
|
|
28
|
+
export { textarea } from './textarea.js'
|
|
29
|
+
export { alert } from './alert.js'
|
|
30
|
+
export { stat } from './stat.js'
|
|
31
|
+
export { avatar } from './avatar.js'
|
|
32
|
+
export { empty } from './empty.js'
|
|
33
|
+
export { table } from './table.js'
|
|
34
|
+
export { hero } from './hero.js'
|
|
35
|
+
export { testimonial } from './testimonial.js'
|
|
36
|
+
export { feature } from './feature.js'
|
|
37
|
+
export { pricing } from './pricing.js'
|
|
38
|
+
export { accordion } from './accordion.js'
|
|
39
|
+
export { nav } from './nav.js'
|
|
40
|
+
export { appBadge } from './app-badge.js'
|
|
41
|
+
export { container } from './container.js'
|
|
42
|
+
export { section } from './section.js'
|
|
43
|
+
export { grid } from './grid.js'
|
|
44
|
+
export { stack } from './stack.js'
|
|
45
|
+
export { cluster } from './cluster.js'
|
|
46
|
+
export { divider } from './divider.js'
|
|
47
|
+
export { banner } from './banner.js'
|
|
48
|
+
export { media } from './media.js'
|
|
49
|
+
export { tooltip } from './tooltip.js'
|
|
50
|
+
export { modal, modalTrigger } from './modal.js'
|
|
51
|
+
export { carousel } from './carousel.js'
|
|
52
|
+
export { cta } from './cta.js'
|
|
53
|
+
export { codeWindow } from './code-window.js'
|
|
54
|
+
export { footer } from './footer.js'
|
|
55
|
+
export { timeline, timelineItem } from './timeline.js'
|
|
56
|
+
export { toggle } from './switch.js'
|
|
57
|
+
export { checkbox } from './checkbox.js'
|
|
58
|
+
export { radio, radioGroup } from './radio.js'
|
|
59
|
+
export { rating } from './rating.js'
|
|
60
|
+
export { spinner } from './spinner.js'
|
|
61
|
+
export { progress } from './progress.js'
|
|
62
|
+
export { barChart, lineChart, donutChart, sparkline } from './charts.js'
|
|
63
|
+
export { slider } from './slider.js'
|
|
64
|
+
export { segmented } from './segmented.js'
|
|
65
|
+
export { fileUpload } from './fileupload.js'
|
|
66
|
+
export { breadcrumbs } from './breadcrumbs.js'
|
|
67
|
+
export { stepper } from './stepper.js'
|
|
68
|
+
export { uiImage } from './uiimage.js'
|
|
69
|
+
export { pullquote } from './pullquote.js'
|
|
70
|
+
export { prose } from './prose.js'
|
|
71
|
+
export { heading } from './heading.js'
|
|
72
|
+
export { list } from './list.js'
|
|
73
|
+
export {
|
|
74
|
+
iconArrowLeft, iconArrowRight, iconArrowUp, iconArrowDown,
|
|
75
|
+
iconChevronLeft, iconChevronRight, iconChevronUp, iconChevronDown,
|
|
76
|
+
iconExternalLink, iconMenu, iconX, iconMoreHorizontal, iconMoreVertical,
|
|
77
|
+
iconCheck, iconCheckCircle, iconXCircle, iconAlertCircle, iconAlertTriangle, iconInfo,
|
|
78
|
+
iconPlus, iconMinus, iconEdit, iconTrash, iconCopy, iconSearch, iconFilter,
|
|
79
|
+
iconDownload, iconUpload, iconRefresh, iconSend,
|
|
80
|
+
iconEye, iconEyeOff, iconLock, iconUnlock, iconSettings, iconBell,
|
|
81
|
+
iconUser, iconUsers, iconMail, iconMessageSquare,
|
|
82
|
+
iconHome, iconLogOut, iconLogIn,
|
|
83
|
+
iconFile, iconImage, iconLink, iconCode, iconCalendar, iconClock, iconBookmark, iconTag,
|
|
84
|
+
iconPlay, iconPause, iconVolume, iconStar, iconHeart,
|
|
85
|
+
iconPhone, iconGamepad,
|
|
86
|
+
iconHandPointUp, iconHandPointDown, iconHandPointLeft, iconHandPointRight,
|
|
87
|
+
iconGlobe, iconShield, iconZap, iconTrendingUp, iconTrendingDown, iconLoader, iconGrid, iconBug, iconMapPin,
|
|
88
|
+
iconSun, iconMoon,
|
|
89
|
+
} from './icons.js'
|
package/src/ui/input.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Input
|
|
3
|
+
*
|
|
4
|
+
* Text input with label, hint, and error message.
|
|
5
|
+
* aria-describedby is wired automatically from name when error/hint are present.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {string} opts.name - Field name (also used as id base)
|
|
9
|
+
* @param {string} opts.label - Visible label text
|
|
10
|
+
* @param {string} opts.type - Input type (default: 'text')
|
|
11
|
+
* @param {string} opts.placeholder
|
|
12
|
+
* @param {string} opts.value - Pre-filled value
|
|
13
|
+
* @param {string} opts.error - Validation error message
|
|
14
|
+
* @param {string} opts.hint - Helper text below the input
|
|
15
|
+
* @param {boolean} opts.required
|
|
16
|
+
* @param {boolean} opts.disabled
|
|
17
|
+
* @param {string} opts.id - Override generated id
|
|
18
|
+
* @param {string} opts.class
|
|
19
|
+
* @param {object} opts.attrs - Extra HTML attributes for the <input>
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { escHtml as e } from '../html.js'
|
|
23
|
+
|
|
24
|
+
export function input({
|
|
25
|
+
name = '',
|
|
26
|
+
label = '',
|
|
27
|
+
type = 'text',
|
|
28
|
+
placeholder = '',
|
|
29
|
+
value = '',
|
|
30
|
+
error = '',
|
|
31
|
+
hint = '',
|
|
32
|
+
required = false,
|
|
33
|
+
disabled = false,
|
|
34
|
+
id = '',
|
|
35
|
+
class: cls = '',
|
|
36
|
+
attrs = {},
|
|
37
|
+
} = {}) {
|
|
38
|
+
const fieldId = e(id || `field-${name}`)
|
|
39
|
+
const errorId = `${fieldId}-error`
|
|
40
|
+
const hintId = `${fieldId}-hint`
|
|
41
|
+
const described = [error ? errorId : '', hint ? hintId : ''].filter(Boolean).join(' ')
|
|
42
|
+
|
|
43
|
+
const wrapClasses = ['ui-field', error ? 'ui-field--error' : '', cls].filter(Boolean).join(' ')
|
|
44
|
+
|
|
45
|
+
const attrsStr = Object.entries(attrs)
|
|
46
|
+
.map(([k, v]) => ` ${e(k)}="${e(String(v))}"`)
|
|
47
|
+
.join('')
|
|
48
|
+
|
|
49
|
+
const labelHtml = label
|
|
50
|
+
? `<label for="${fieldId}" class="ui-label">${e(label)}${required ? ' <span class="ui-required" aria-hidden="true">*</span>' : ''}</label>`
|
|
51
|
+
: ''
|
|
52
|
+
|
|
53
|
+
const hintHtml = hint ? `<p id="${hintId}" class="ui-hint">${e(hint)}</p>` : ''
|
|
54
|
+
const errorHtml = error ? `<p id="${errorId}" class="ui-error" role="alert">${e(error)}</p>` : ''
|
|
55
|
+
|
|
56
|
+
return `<div class="${e(wrapClasses)}">
|
|
57
|
+
${labelHtml}
|
|
58
|
+
<input
|
|
59
|
+
id="${fieldId}"
|
|
60
|
+
name="${e(name)}"
|
|
61
|
+
type="${e(type)}"
|
|
62
|
+
class="ui-input"
|
|
63
|
+
${placeholder ? `placeholder="${e(placeholder)}"` : ''}
|
|
64
|
+
${value ? `value="${e(value)}"` : ''}
|
|
65
|
+
${required ? 'required aria-required="true"' : ''}
|
|
66
|
+
${disabled ? 'disabled' : ''}
|
|
67
|
+
${described ? `aria-describedby="${described}"` : ''}
|
|
68
|
+
${error ? 'aria-invalid="true"' : ''}
|
|
69
|
+
${attrsStr}
|
|
70
|
+
>
|
|
71
|
+
${hintHtml}
|
|
72
|
+
${errorHtml}
|
|
73
|
+
</div>`
|
|
74
|
+
}
|
package/src/ui/list.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — List
|
|
3
|
+
*
|
|
4
|
+
* Styled ordered or unordered list. Use this instead of raw <ul>/<ol> to get
|
|
5
|
+
* consistent spacing, bullets, and colour tokens.
|
|
6
|
+
*
|
|
7
|
+
* Items are HTML strings — escape user data before passing them in.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} opts
|
|
10
|
+
* @param {string[]} opts.items - Array of HTML strings for each list item
|
|
11
|
+
* @param {boolean} [opts.ordered] - true for <ol>, false for <ul> (default: false)
|
|
12
|
+
* @param {string} [opts.gap] - Item spacing: 'xs'|'sm'|'md' (default: 'sm')
|
|
13
|
+
* @param {string} [opts.class] - Extra classes on the list element
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export function list({
|
|
17
|
+
items = [],
|
|
18
|
+
ordered = false,
|
|
19
|
+
gap = 'sm',
|
|
20
|
+
class: cls = '',
|
|
21
|
+
} = {}) {
|
|
22
|
+
if (!items.length) return ''
|
|
23
|
+
|
|
24
|
+
const tag = ordered ? 'ol' : 'ul'
|
|
25
|
+
const gapMap = { xs: 'u-gap-1', sm: 'u-gap-2', md: 'u-gap-3' }
|
|
26
|
+
const classes = [
|
|
27
|
+
'ui-list',
|
|
28
|
+
ordered ? 'ui-list--ordered' : 'ui-list--unordered',
|
|
29
|
+
gapMap[gap] || 'u-gap-2',
|
|
30
|
+
cls,
|
|
31
|
+
].filter(Boolean).join(' ')
|
|
32
|
+
|
|
33
|
+
const itemsHtml = items.map(item => `<li class="ui-list-item">${item}</li>`).join('')
|
|
34
|
+
|
|
35
|
+
return `<${tag} class="${classes}">${itemsHtml}</${tag}>`
|
|
36
|
+
}
|
package/src/ui/media.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Media
|
|
3
|
+
*
|
|
4
|
+
* Image + text side-by-side layout. Stacks vertically on mobile.
|
|
5
|
+
* The image slot accepts any HTML — <img>, <figure>, an SVG, or a styled div.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {string} opts.image - Raw HTML slot — the visual side
|
|
9
|
+
* @param {string} opts.content - Raw HTML slot — the text side
|
|
10
|
+
* @param {boolean} opts.reverse - Put text left, image right (default: false)
|
|
11
|
+
* @param {'start'|'center'} opts.align - Vertical alignment of columns (default: 'center')
|
|
12
|
+
* @param {'sm'|'md'|'lg'} opts.gap - Gap between columns (default: 'md')
|
|
13
|
+
* @param {string} opts.class
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { escHtml as e } from '../html.js'
|
|
17
|
+
|
|
18
|
+
const ALIGNS = new Set(['start', 'center'])
|
|
19
|
+
const GAPS = new Set(['sm', 'md', 'lg'])
|
|
20
|
+
|
|
21
|
+
export function media({
|
|
22
|
+
image = '',
|
|
23
|
+
content = '',
|
|
24
|
+
reverse = false,
|
|
25
|
+
align = 'center',
|
|
26
|
+
gap = 'md',
|
|
27
|
+
class: cls = '',
|
|
28
|
+
} = {}) {
|
|
29
|
+
if (!ALIGNS.has(align)) align = 'center'
|
|
30
|
+
if (!GAPS.has(gap)) gap = 'md'
|
|
31
|
+
|
|
32
|
+
const classes = [
|
|
33
|
+
'ui-media',
|
|
34
|
+
reverse && 'ui-media--reverse',
|
|
35
|
+
align !== 'center' && `ui-media--align-${align}`,
|
|
36
|
+
gap !== 'md' && `ui-media--gap-${gap}`,
|
|
37
|
+
cls,
|
|
38
|
+
].filter(Boolean).join(' ')
|
|
39
|
+
|
|
40
|
+
return `<div class="${e(classes)}">
|
|
41
|
+
<div class="ui-media-image">${image}</div>
|
|
42
|
+
<div class="ui-media-content">${content}</div>
|
|
43
|
+
</div>`
|
|
44
|
+
}
|
package/src/ui/modal.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Modal
|
|
3
|
+
*
|
|
4
|
+
* A <dialog>-based modal. Use modal() to render the dialog element and
|
|
5
|
+
* modalTrigger() (or any element with data-modal-open="id") to open it.
|
|
6
|
+
*
|
|
7
|
+
* Requires pulse-ui.js for the open trigger.
|
|
8
|
+
* The close button inside the dialog uses native <form method="dialog"> — no JS needed.
|
|
9
|
+
* Clicking the backdrop also closes the modal (handled by pulse-ui.js).
|
|
10
|
+
*
|
|
11
|
+
* @param {object} opts
|
|
12
|
+
* @param {string} opts.id - Unique ID — required so triggers can target this dialog
|
|
13
|
+
* @param {string} opts.title - Dialog heading
|
|
14
|
+
* @param {number} opts.level - Heading level 1–6 (default 2). Visual style is always ui-modal-title.
|
|
15
|
+
* @param {string} opts.content - Body HTML
|
|
16
|
+
* @param {string} opts.footer - Footer HTML — typically button() calls
|
|
17
|
+
* @param {'sm'|'md'|'lg'|'xl'} opts.size
|
|
18
|
+
* @param {string} opts.class
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { escHtml as e } from '../html.js'
|
|
22
|
+
import { iconX } from './icons.js'
|
|
23
|
+
|
|
24
|
+
const SIZES = new Set(['sm', 'md', 'lg', 'xl'])
|
|
25
|
+
|
|
26
|
+
export function modal({
|
|
27
|
+
id = '',
|
|
28
|
+
title = '',
|
|
29
|
+
level = 2,
|
|
30
|
+
content = '',
|
|
31
|
+
footer = '',
|
|
32
|
+
size = 'md',
|
|
33
|
+
class: cls = '',
|
|
34
|
+
} = {}) {
|
|
35
|
+
if (!SIZES.has(size)) size = 'md'
|
|
36
|
+
|
|
37
|
+
const classes = ['ui-modal', size !== 'md' && `ui-modal--${size}`, cls].filter(Boolean).join(' ')
|
|
38
|
+
const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
|
|
39
|
+
|
|
40
|
+
return `<dialog class="${e(classes)}"${id ? ` id="${e(id)}"` : ''}${id ? ` aria-labelledby="${e(id)}-title"` : ''}>
|
|
41
|
+
<form method="dialog" class="ui-modal-inner">
|
|
42
|
+
<header class="ui-modal-header">
|
|
43
|
+
<${tag} class="ui-modal-title"${id ? ` id="${e(id)}-title"` : ''}>${e(title)}</${tag}>
|
|
44
|
+
<button type="submit" class="ui-modal-close" aria-label="Close dialog">
|
|
45
|
+
${iconX({ size: 16 })}
|
|
46
|
+
</button>
|
|
47
|
+
</header>
|
|
48
|
+
<div class="ui-modal-body">${content}</div>
|
|
49
|
+
${footer ? `<footer class="ui-modal-footer">${footer}</footer>` : ''}
|
|
50
|
+
</form>
|
|
51
|
+
</dialog>`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Button that opens a modal by ID.
|
|
56
|
+
* Any element with data-modal-open="<id>" also works with pulse-ui.js.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} opts
|
|
59
|
+
* @param {string} opts.target - The modal's id attribute
|
|
60
|
+
* @param {string} opts.label - Button label
|
|
61
|
+
* @param {'primary'|'secondary'|'ghost'|'danger'} opts.variant
|
|
62
|
+
* @param {'sm'|'md'|'lg'} opts.size
|
|
63
|
+
* @param {string} opts.class
|
|
64
|
+
*/
|
|
65
|
+
export function modalTrigger({
|
|
66
|
+
target = '',
|
|
67
|
+
label = 'Open',
|
|
68
|
+
variant = 'primary',
|
|
69
|
+
size = 'md',
|
|
70
|
+
class: cls = '',
|
|
71
|
+
} = {}) {
|
|
72
|
+
const VARIANTS = new Set(['primary', 'secondary', 'ghost', 'danger'])
|
|
73
|
+
const BTN_SIZES = new Set(['sm', 'md', 'lg'])
|
|
74
|
+
if (!VARIANTS.has(variant)) variant = 'primary'
|
|
75
|
+
if (!BTN_SIZES.has(size)) size = 'md'
|
|
76
|
+
|
|
77
|
+
const classes = ['ui-btn', `ui-btn--${variant}`, `ui-btn--${size}`, cls].filter(Boolean).join(' ')
|
|
78
|
+
|
|
79
|
+
return `<button type="button" class="${e(classes)}" data-dialog-open="${e(target)}"><span>${e(label)}</span></button>`
|
|
80
|
+
}
|
package/src/ui/nav.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Nav
|
|
3
|
+
*
|
|
4
|
+
* Site header with logo, navigation links, and an optional CTA slot.
|
|
5
|
+
* Set sticky: true for a position: sticky top bar with backdrop blur.
|
|
6
|
+
*
|
|
7
|
+
* On mobile (< 640px) links are hidden behind a burger button. Clicking
|
|
8
|
+
* the burger toggles the .ui-nav--open class on the <header>, which
|
|
9
|
+
* reveals an absolutely-positioned dropdown that overlays page content.
|
|
10
|
+
* Wired automatically by pulse-ui.js — no extra JS needed.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {string} opts.logo - Raw HTML slot — SVG, img, or text
|
|
14
|
+
* @param {string} opts.logoHref - Logo link destination (default: '/')
|
|
15
|
+
* @param {Array<{label: string, href: string}>} opts.links
|
|
16
|
+
* @param {string} opts.action - Raw HTML slot — typically a button()
|
|
17
|
+
* @param {boolean} opts.sticky - Position sticky with backdrop blur
|
|
18
|
+
* @param {'right'|'left'} opts.burgerAlign - Mobile burger position (default: 'right')
|
|
19
|
+
* @param {string} opts.class
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { escHtml as e } from '../html.js'
|
|
23
|
+
import { iconMenu, iconX } from './icons.js'
|
|
24
|
+
|
|
25
|
+
let _navId = 0
|
|
26
|
+
|
|
27
|
+
export function nav({
|
|
28
|
+
logo = '',
|
|
29
|
+
logoHref = '/',
|
|
30
|
+
links = [],
|
|
31
|
+
action = '',
|
|
32
|
+
sticky = false,
|
|
33
|
+
burgerAlign = 'right',
|
|
34
|
+
class: cls = '',
|
|
35
|
+
} = {}) {
|
|
36
|
+
const id = `ui-nav-${++_navId}`
|
|
37
|
+
const classes = ['ui-nav', sticky && 'ui-nav--sticky', burgerAlign === 'left' && 'ui-nav--burger-left', cls].filter(Boolean).join(' ')
|
|
38
|
+
|
|
39
|
+
const linksHtml = links.map(({ label = '', href = '' }) =>
|
|
40
|
+
`<a href="${e(href)}" class="ui-nav-link">${e(label)}</a>`
|
|
41
|
+
).join('')
|
|
42
|
+
|
|
43
|
+
const burgerHtml = links.length ? `
|
|
44
|
+
<button class="ui-nav-burger" type="button" aria-label="Toggle menu" aria-expanded="false" aria-controls="${id}-mobile">
|
|
45
|
+
${iconMenu({ size: 20, class: 'ui-nav-burger-open' })}
|
|
46
|
+
${iconX({ size: 20, class: 'ui-nav-burger-close' })}
|
|
47
|
+
</button>` : ''
|
|
48
|
+
|
|
49
|
+
const mobileMenuHtml = links.length ? `
|
|
50
|
+
<div class="ui-nav-mobile" id="${id}-mobile" aria-label="Mobile navigation">
|
|
51
|
+
<nav>${linksHtml}</nav>
|
|
52
|
+
</div>` : ''
|
|
53
|
+
|
|
54
|
+
return `<header class="${e(classes)}">
|
|
55
|
+
<div class="ui-nav-inner">
|
|
56
|
+
<a href="${e(logoHref)}" class="ui-nav-logo">${logo}</a>
|
|
57
|
+
${links.length ? `<nav class="ui-nav-links" aria-label="Site navigation">${linksHtml}</nav>` : ''}
|
|
58
|
+
${action ? `<div class="ui-nav-action">${action}</div>` : ''}${burgerHtml}
|
|
59
|
+
</div>${mobileMenuHtml}
|
|
60
|
+
</header>`
|
|
61
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Pricing
|
|
3
|
+
*
|
|
4
|
+
* A single pricing plan card with feature list and CTA.
|
|
5
|
+
* Compose multiple in a flex/grid container for a full pricing section.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {string} opts.name - Plan name (e.g. "Pro")
|
|
9
|
+
* @param {number} opts.level - Heading level 1–6 for the plan name (default 3). Visual style is always ui-pricing-name.
|
|
10
|
+
* @param {string} opts.price - Price string (e.g. "$9", "Free")
|
|
11
|
+
* @param {string} opts.period - Billing period (e.g. "/month")
|
|
12
|
+
* @param {string} opts.description - One-line plan description
|
|
13
|
+
* @param {string[]} opts.features - Array of feature strings
|
|
14
|
+
* @param {string} opts.action - Raw HTML slot — typically a button()
|
|
15
|
+
* @param {boolean} opts.highlighted - Accent border + elevated appearance
|
|
16
|
+
* @param {string} opts.badge - Floating label above the card (e.g. "Most popular")
|
|
17
|
+
* @param {string} opts.class
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { escHtml as e } from '../html.js'
|
|
21
|
+
|
|
22
|
+
export function pricing({
|
|
23
|
+
name = '',
|
|
24
|
+
level = 3,
|
|
25
|
+
price = '',
|
|
26
|
+
period = '',
|
|
27
|
+
description = '',
|
|
28
|
+
features = [],
|
|
29
|
+
action = '',
|
|
30
|
+
highlighted = false,
|
|
31
|
+
badge = '',
|
|
32
|
+
class: cls = '',
|
|
33
|
+
} = {}) {
|
|
34
|
+
const classes = ['ui-pricing', highlighted && 'ui-pricing--highlighted', cls].filter(Boolean).join(' ')
|
|
35
|
+
const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
|
|
36
|
+
|
|
37
|
+
const featureList = features.length
|
|
38
|
+
? `<ul class="ui-pricing-features">
|
|
39
|
+
${features.map(f => `<li class="ui-pricing-feature"><span class="ui-pricing-check" aria-hidden="true">✓</span>${e(f)}</li>`).join('\n ')}
|
|
40
|
+
</ul>`
|
|
41
|
+
: ''
|
|
42
|
+
|
|
43
|
+
return `<div class="${e(classes)}">
|
|
44
|
+
${badge ? `<p class="ui-pricing-badge">${e(badge)}</p>` : ''}
|
|
45
|
+
<div class="ui-pricing-header">
|
|
46
|
+
${name ? `<${tag} class="ui-pricing-name">${e(name)}</${tag}>` : ''}
|
|
47
|
+
${description ? `<p class="ui-pricing-desc">${e(description)}</p>` : ''}
|
|
48
|
+
</div>
|
|
49
|
+
<div class="ui-pricing-price">
|
|
50
|
+
<span class="ui-pricing-amount">${e(price)}</span>
|
|
51
|
+
${period ? `<span class="ui-pricing-period">${e(period)}</span>` : ''}
|
|
52
|
+
</div>
|
|
53
|
+
${featureList}
|
|
54
|
+
${action ? `<div class="ui-pricing-action">${action}</div>` : ''}
|
|
55
|
+
</div>`
|
|
56
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Progress
|
|
3
|
+
*
|
|
4
|
+
* Horizontal progress bar. Supports determinate and indeterminate states.
|
|
5
|
+
*
|
|
6
|
+
* @param {object} opts
|
|
7
|
+
* @param {number} opts.value - Current value (0–max). Omit for indeterminate.
|
|
8
|
+
* @param {number} opts.max - Maximum value (default: 100)
|
|
9
|
+
* @param {string} opts.label - Accessible label
|
|
10
|
+
* @param {boolean} opts.showLabel - Render label text above the bar
|
|
11
|
+
* @param {boolean} opts.showValue - Render percentage above the bar (right-aligned)
|
|
12
|
+
* @param {'accent'|'success'|'warning'|'error'} opts.variant
|
|
13
|
+
* @param {'sm'|'md'|'lg'} opts.size
|
|
14
|
+
* @param {string} opts.class
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { escHtml as e } from '../html.js'
|
|
18
|
+
|
|
19
|
+
const VARIANTS = { accent: 'accent', success: 'success', warning: 'warning', error: 'error' }
|
|
20
|
+
const SIZES = { sm: '.25rem', md: '.5rem', lg: '1rem' }
|
|
21
|
+
|
|
22
|
+
export function progress({
|
|
23
|
+
value = undefined,
|
|
24
|
+
max = 100,
|
|
25
|
+
label = '',
|
|
26
|
+
showLabel = false,
|
|
27
|
+
showValue = false,
|
|
28
|
+
variant = 'accent',
|
|
29
|
+
size = 'md',
|
|
30
|
+
class: cls = '',
|
|
31
|
+
} = {}) {
|
|
32
|
+
const indeterminate = value === undefined || value === null
|
|
33
|
+
const clamped = indeterminate ? 0 : Math.min(Math.max(Number(value), 0), max)
|
|
34
|
+
const pct = indeterminate ? 0 : Math.round((clamped / max) * 100)
|
|
35
|
+
const v = VARIANTS[variant] ?? 'accent'
|
|
36
|
+
const h = SIZES[size] ?? SIZES.md
|
|
37
|
+
|
|
38
|
+
const classes = [
|
|
39
|
+
'ui-progress',
|
|
40
|
+
`ui-progress--${v}`,
|
|
41
|
+
indeterminate ? 'ui-progress--indeterminate' : '',
|
|
42
|
+
cls,
|
|
43
|
+
].filter(Boolean).join(' ')
|
|
44
|
+
|
|
45
|
+
const header = (showLabel || showValue) ? `
|
|
46
|
+
<div class="ui-progress-header">
|
|
47
|
+
${showLabel && label ? `<span class="ui-progress-label">${e(label)}</span>` : '<span></span>'}
|
|
48
|
+
${showValue && !indeterminate ? `<span class="ui-progress-value">${pct}%</span>` : ''}
|
|
49
|
+
</div>` : ''
|
|
50
|
+
|
|
51
|
+
return `<div
|
|
52
|
+
class="${e(classes)}"
|
|
53
|
+
role="progressbar"
|
|
54
|
+
${!indeterminate ? `aria-valuenow="${clamped}" aria-valuemin="0" aria-valuemax="${max}"` : ''}
|
|
55
|
+
${label ? `aria-label="${e(label)}"` : ''}
|
|
56
|
+
style="--progress-height:${h}"
|
|
57
|
+
>${header}
|
|
58
|
+
<div class="ui-progress-track">
|
|
59
|
+
<div class="ui-progress-fill" style="${indeterminate ? '' : `width:${pct}%`}"></div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>`
|
|
62
|
+
}
|
package/src/ui/prose.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Prose
|
|
3
|
+
*
|
|
4
|
+
* Typography wrapper for rich text content: CMS output, markdown-rendered HTML,
|
|
5
|
+
* blog posts, article bodies, legal text, etc.
|
|
6
|
+
*
|
|
7
|
+
* Styles all descendant elements (h1–h6, p, ul, ol, li, a, blockquote, code,
|
|
8
|
+
* pre, table, hr, img) without requiring classes on individual elements.
|
|
9
|
+
* Use this any time you're outputting HTML you don't control.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} opts
|
|
12
|
+
* @param {string} opts.content - Raw HTML string (NOT escaped — trust only server-side content)
|
|
13
|
+
* @param {string} [opts.size] - 'sm' | 'base' | 'lg' (default: 'base')
|
|
14
|
+
* @param {string} [opts.class] - Extra classes on the wrapper
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export function prose({
|
|
18
|
+
content = '',
|
|
19
|
+
size = 'base',
|
|
20
|
+
class: cls = '',
|
|
21
|
+
} = {}) {
|
|
22
|
+
const classes = [
|
|
23
|
+
'ui-prose',
|
|
24
|
+
size !== 'base' ? `ui-prose--${size}` : '',
|
|
25
|
+
cls,
|
|
26
|
+
].filter(Boolean).join(' ')
|
|
27
|
+
|
|
28
|
+
return `<div class="${classes}">${content}</div>`
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Pullquote
|
|
3
|
+
*
|
|
4
|
+
* Styled blockquote with accent left border and optional attribution.
|
|
5
|
+
*
|
|
6
|
+
* @param {object} opts
|
|
7
|
+
* @param {string} opts.quote - The quote text
|
|
8
|
+
* @param {string} opts.cite - Attribution (name, role, etc.)
|
|
9
|
+
* @param {'md'|'lg'} opts.size - Size variant (default: 'md')
|
|
10
|
+
* @param {string} opts.class
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { escHtml as e } from '../html.js'
|
|
14
|
+
|
|
15
|
+
export function pullquote({
|
|
16
|
+
quote = '',
|
|
17
|
+
cite = '',
|
|
18
|
+
size = 'md',
|
|
19
|
+
class: cls = '',
|
|
20
|
+
} = {}) {
|
|
21
|
+
const sizeClass = size === 'lg' ? 'ui-pullquote--lg' : ''
|
|
22
|
+
const figClasses = ['ui-pullquote', sizeClass, cls].filter(Boolean).join(' ')
|
|
23
|
+
|
|
24
|
+
const citeHtml = cite
|
|
25
|
+
? `<figcaption class="ui-pullquote-cite">${e(cite)}</figcaption>`
|
|
26
|
+
: ''
|
|
27
|
+
|
|
28
|
+
return `<figure class="${e(figClasses)}">
|
|
29
|
+
<blockquote>
|
|
30
|
+
<p class="ui-pullquote-text">${e(quote)}</p>
|
|
31
|
+
</blockquote>
|
|
32
|
+
${citeHtml}
|
|
33
|
+
</figure>`
|
|
34
|
+
}
|
package/src/ui/radio.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Radio & RadioGroup
|
|
3
|
+
*
|
|
4
|
+
* radio() — single radio button with label
|
|
5
|
+
* radioGroup() — semantic fieldset of radio options
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts (radio)
|
|
8
|
+
* @param {string} opts.name - Field name
|
|
9
|
+
* @param {string} opts.value - Submitted value
|
|
10
|
+
* @param {string} opts.label - Visible label
|
|
11
|
+
* @param {boolean} opts.checked
|
|
12
|
+
* @param {boolean} opts.disabled
|
|
13
|
+
* @param {string} opts.id - Override generated id
|
|
14
|
+
* @param {string} opts.class
|
|
15
|
+
*
|
|
16
|
+
* @param {object} opts (radioGroup)
|
|
17
|
+
* @param {string} opts.name - Shared field name
|
|
18
|
+
* @param {string} opts.legend - Group label (renders as <legend>)
|
|
19
|
+
* @param {Array} opts.options - [{ value, label, hint?, disabled? }]
|
|
20
|
+
* @param {string} opts.value - Currently selected value
|
|
21
|
+
* @param {string} opts.hint - Helper text below the group
|
|
22
|
+
* @param {string} opts.error - Validation error message
|
|
23
|
+
* @param {'sm'|'md'|'lg'} opts.gap - Gap between options (default: 'md')
|
|
24
|
+
* @param {string} opts.class
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { escHtml as e } from '../html.js'
|
|
28
|
+
|
|
29
|
+
export function radio({
|
|
30
|
+
name = '',
|
|
31
|
+
value = '',
|
|
32
|
+
label = '',
|
|
33
|
+
checked = false,
|
|
34
|
+
disabled = false,
|
|
35
|
+
id = '',
|
|
36
|
+
event = '',
|
|
37
|
+
class: cls = '',
|
|
38
|
+
} = {}) {
|
|
39
|
+
const fieldId = e(id || `radio-${name}-${value}`)
|
|
40
|
+
const classes = ['ui-radio', cls].filter(Boolean).join(' ')
|
|
41
|
+
|
|
42
|
+
return `<label class="${e(classes)}${disabled ? ' ui-radio--disabled' : ''}">
|
|
43
|
+
<input
|
|
44
|
+
type="radio"
|
|
45
|
+
id="${fieldId}"
|
|
46
|
+
name="${e(name)}"
|
|
47
|
+
value="${e(value)}"
|
|
48
|
+
class="ui-radio-input"
|
|
49
|
+
${checked ? 'checked' : ''}
|
|
50
|
+
${disabled ? 'disabled' : ''}
|
|
51
|
+
${event ? `data-event="${e(event)}"` : ''}
|
|
52
|
+
>
|
|
53
|
+
<span class="ui-radio-dot" aria-hidden="true"></span>
|
|
54
|
+
${label ? `<span class="ui-radio-label">${e(label)}</span>` : ''}
|
|
55
|
+
</label>`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const GAPS = { sm: '.375rem', md: '.75rem', lg: '1.25rem' }
|
|
59
|
+
|
|
60
|
+
export function radioGroup({
|
|
61
|
+
name = '',
|
|
62
|
+
legend = '',
|
|
63
|
+
options = [],
|
|
64
|
+
value = '',
|
|
65
|
+
hint = '',
|
|
66
|
+
error = '',
|
|
67
|
+
gap = 'md',
|
|
68
|
+
event = '',
|
|
69
|
+
class: cls = '',
|
|
70
|
+
} = {}) {
|
|
71
|
+
const groupId = `radiogroup-${name}`
|
|
72
|
+
const errorId = `${groupId}-error`
|
|
73
|
+
const hintId = `${groupId}-hint`
|
|
74
|
+
const gapValue = GAPS[gap] ?? GAPS.md
|
|
75
|
+
|
|
76
|
+
const described = [error ? errorId : '', hint ? hintId : ''].filter(Boolean).join(' ')
|
|
77
|
+
|
|
78
|
+
const items = options.map(opt =>
|
|
79
|
+
radio({
|
|
80
|
+
name,
|
|
81
|
+
value: opt.value,
|
|
82
|
+
label: opt.label,
|
|
83
|
+
checked: String(opt.value) === String(value),
|
|
84
|
+
disabled: opt.disabled ?? false,
|
|
85
|
+
event,
|
|
86
|
+
}) + (opt.hint ? `<p class="ui-hint" style="margin:-.25rem 0 0 2rem">${e(opt.hint)}</p>` : '')
|
|
87
|
+
).join('')
|
|
88
|
+
|
|
89
|
+
const hintHtml = hint ? `<p id="${hintId}" class="ui-hint">${e(hint)}</p>` : ''
|
|
90
|
+
const errorHtml = error ? `<p id="${errorId}" class="ui-error" role="alert">${e(error)}</p>` : ''
|
|
91
|
+
|
|
92
|
+
const classes = ['ui-radio-group', error ? 'ui-radio-group--error' : '', cls].filter(Boolean).join(' ')
|
|
93
|
+
|
|
94
|
+
return `<fieldset class="${e(classes)}"${described ? ` aria-describedby="${described}"` : ''}>
|
|
95
|
+
${legend ? `<legend class="ui-fieldset-legend">${e(legend)}</legend>` : ''}
|
|
96
|
+
<div class="ui-radio-group-body" style="--radio-gap:${gapValue}">
|
|
97
|
+
${items}
|
|
98
|
+
${hintHtml}
|
|
99
|
+
${errorHtml}
|
|
100
|
+
</div>
|
|
101
|
+
</fieldset>`
|
|
102
|
+
}
|