@viveksinghind/narrative-form-vue 1.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vivek Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @viveksinghind/narrative-form-vue
2
+
3
+ Vue 3 implementation for Narrative Form, a dynamic typewriter-style sign-up flow.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @viveksinghind/narrative-form-vue @viveksinghind/narrative-form-core
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```vue
14
+ <script setup>
15
+ import { NarrativeForm } from '@viveksinghind/narrative-form-vue';
16
+
17
+ const myFormConfig = {
18
+ form: { id: "test", name: "Test Form", version: 1 },
19
+ welcome: {
20
+ heading: "Welcome to the future",
21
+ subtext: "Let's get started",
22
+ ctaLabel: "Begin"
23
+ },
24
+ fields: [
25
+ {
26
+ key: "name",
27
+ type: "text",
28
+ prefix: "Hi, my name is",
29
+ validation: { required: true }
30
+ }
31
+ ],
32
+ done: {
33
+ message: "All done! Thank you.",
34
+ }
35
+ };
36
+
37
+ const handleComplete = (values) => {
38
+ console.log('Form Complete!', values);
39
+ };
40
+ </script>
41
+
42
+ <template>
43
+ <NarrativeForm
44
+ :config="myFormConfig"
45
+ @complete="handleComplete"
46
+ />
47
+ </template>
48
+ ```
49
+
50
+ ## Features
51
+ - Built for Vue 3 Composition API
52
+ - Uses lightweight `shallowRef` reactivity bridge
53
+ - Smooth CSS animations
54
+ - Themeable via config
@@ -0,0 +1,15 @@
1
+ import { NarrativeField, FormStateSnapshot, NarrativeFieldValues, NarrativeMeta } from '@viveksinghind/narrative-form-core';
2
+ export declare function useFormState(fields: readonly NarrativeField[]): {
3
+ snapshot: import('vue').ShallowRef<FormStateSnapshot, FormStateSnapshot>;
4
+ startTyping: (key: string) => void;
5
+ activateField: (key: string) => void;
6
+ confirmField: (key: string, value: string | string[]) => void;
7
+ editField: (key: string) => void;
8
+ reconfirmField: (key: string, value: string | string[]) => void;
9
+ next: () => void;
10
+ focusField: (key: string) => void;
11
+ reset: () => void;
12
+ getValues: () => NarrativeFieldValues;
13
+ getMeta: (formId?: string, formVersion?: number) => NarrativeMeta;
14
+ };
15
+ //# sourceMappingURL=useFormState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFormState.d.ts","sourceRoot":"","sources":["../../src/composables/useFormState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAGjI,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE;;uBAcvC,MAAM;yBACJ,MAAM;wBACP,MAAM,SAAS,MAAM,GAAG,MAAM,EAAE;qBACnC,MAAM;0BACD,MAAM,SAAS,MAAM,GAAG,MAAM,EAAE;;sBAEpC,MAAM;;;uBAML,MAAM,gBAAgB,MAAM;EAElD"}
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("vue"),C=require("@viveksinghind/narrative-form-core");function I(l){const t=new C.FormStateEngine(l,()=>{n.value=t.getSnapshot(),e.triggerRef(n)}),n=e.shallowRef(t.getSnapshot());return e.onUnmounted(()=>{t.reset()}),{snapshot:n,startTyping:o=>t.startTyping(o),activateField:o=>t.activateField(o),confirmField:(o,u)=>t.confirmField(o,u),editField:o=>t.editField(o),reconfirmField:(o,u)=>t.reconfirmField(o,u),next:()=>t.next(),focusField:o=>t.focusField(o),reset:()=>{t.reset(),n.value=t.getSnapshot()},getValues:()=>t.getValues(),getMeta:(o,u)=>t.getMeta(o,u)}}const R={class:"ns-welcome fade-in"},W={class:"ns-welcome-title"},K={key:0,class:"ns-welcome-subtitle"},U=e.defineComponent({__name:"WelcomeScreen",props:{welcome:{},typewriter:{}},emits:["start"],setup(l,{emit:t}){const n=l,o=t,u=e.ref("");let p;return e.onMounted(()=>{if(!n.typewriter.enabled){u.value=n.welcome.heading||"";return}let m=0;p=setInterval(()=>{u.value=(n.welcome.heading||"").slice(0,m+1),m++,m>=(n.welcome.heading||"").length&&clearInterval(p)},n.typewriter.speed??30)}),e.onUnmounted(()=>{clearInterval(p)}),(m,s)=>(e.openBlock(),e.createElementBlock("div",R,[e.createElementVNode("h1",W,e.toDisplayString(u.value),1),l.welcome.subtext?(e.openBlock(),e.createElementBlock("p",K,e.toDisplayString(l.welcome.subtext),1)):e.createCommentVNode("",!0),e.createElementVNode("button",{type:"button",class:"ns-welcome-start",onClick:s[0]||(s[0]=g=>o("start"))},e.toDisplayString(l.welcome.ctaLabel||"Start"),1)]))}}),B=(l,t)=>{const n=l.__vccOpts||l;for(const[o,u]of t)n[o]=u;return n},D=B(U,[["__scopeId","data-v-dbeb46ea"]]),q={class:"ns-done fade-in"},A={class:"ns-done-title"},O=e.defineComponent({__name:"DoneScreen",props:{done:{},values:{},meta:{},typewriter:{}},setup(l){return(t,n)=>(e.openBlock(),e.createElementBlock("div",q,[e.createElementVNode("h1",A,e.toDisplayString(typeof l.done.message=="function"?l.done.message(l.values):l.done.message),1)]))}}),M=B(O,[["__scopeId","data-v-7802c8f5"]]),P={key:0,class:"ns-error-wrapper"},j={class:"ns-error-text"},H=e.defineComponent({__name:"ErrorMessage",props:{message:{},config:{}},setup(l){return(t,n)=>(e.openBlock(),e.createBlock(e.Transition,{name:"fade-slide"},{default:e.withCtx(()=>[l.message?(e.openBlock(),e.createElementBlock("div",P,[e.createElementVNode("p",j,e.toDisplayString(l.message),1)])):e.createCommentVNode("",!0)]),_:1}))}}),_=B(H,[["__scopeId","data-v-a8be2d7b"]]),z={class:"ns-line fade-in"},G={class:"ns-prompt-row"},J={class:"ns-prompt"},Q={key:0,class:"ns-value"},X={key:0,class:"ns-input-row"},Y=["type","disabled","value","onKeydown"],Z=e.defineComponent({__name:"Line",props:{field:{},status:{},value:{},allValues:{},typewriter:{},editable:{type:Boolean},locked:{type:Boolean},editLabel:{}},emits:["typingComplete","confirm","edit","error","change","focus","blur"],setup(l,{emit:t}){const n=l,o=t,u=e.ref(null),p=e.ref(""),m=e.ref(n.value?String(n.value):""),s=e.ref(null),g=e.ref(!1);let w;e.watch(()=>n.value,f=>{f!==void 0&&String(f)!==m.value&&(m.value=String(f))}),e.watch(()=>n.status,f=>{if(f==="typing"){if(!n.typewriter.enabled){p.value=n.field.prompt,o("typingComplete",n.field.key);return}let i=0;w=setInterval(()=>{p.value=n.field.prompt.slice(0,i+1),i++,i>=n.field.prompt.length&&(clearInterval(w),o("typingComplete",n.field.key))},n.typewriter.speed??30)}else(f==="active"||f==="editing")&&e.nextTick(()=>{var i;(i=u.value)==null||i.focus()})},{immediate:!0}),e.onUnmounted(()=>{clearInterval(w)});const E=f=>{const i=f.target;m.value=i.value,o("change",n.field.key,i.value),s.value&&(s.value=null)},V=async()=>{const f=C.validateField(n.field,m.value);if(!f.valid){s.value=f.error||"Invalid input",o("error",n.field.key,s.value);return}if(C.hasAsyncValidation(n.field)){g.value=!0;const i=await C.validateFieldAsync(n.field,m.value);if(g.value=!1,!i.valid){s.value=i.error||"Invalid input",o("error",n.field.key,s.value);return}}s.value=null,o("confirm",n.field.key,m.value)};return(f,i)=>(e.openBlock(),e.createElementBlock("div",z,[e.createElementVNode("div",G,[e.createElementVNode("span",J,e.toDisplayString(l.status==="typing"?p.value:l.field.prompt),1),l.status==="confirmed"?(e.openBlock(),e.createElementBlock("span",Q,e.toDisplayString(m.value),1)):e.createCommentVNode("",!0),l.status==="confirmed"&&l.editable&&!l.locked?(e.openBlock(),e.createElementBlock("button",{key:1,type:"button",class:"ns-edit-btn",onClick:i[0]||(i[0]=F=>o("edit",l.field.key))},e.toDisplayString(l.editLabel),1)):e.createCommentVNode("",!0)]),l.status==="active"||l.status==="editing"?(e.openBlock(),e.createElementBlock("div",X,[e.createElementVNode("input",{ref_key:"inputRef",ref:u,type:l.field.type==="email"?"email":l.field.type==="password"?"password":"text",class:"ns-input",disabled:g.value,value:m.value,onInput:E,onFocus:i[1]||(i[1]=F=>o("focus",l.field.key)),onBlur:i[2]||(i[2]=F=>o("blur",l.field.key,m.value)),onKeydown:e.withKeys(e.withModifiers(V,["prevent"]),["enter"])},null,40,Y)])):e.createCommentVNode("",!0),e.createVNode(_,{message:s.value},null,8,["message"])]))}}),L=B(Z,[["__scopeId","data-v-d4b482a6"]]),ee={key:1,class:"ns-form-body"},te=e.defineComponent({__name:"NarrativeForm",props:{fields:{},formConfig:{},typewriter:{},welcome:{},done:{},editable:{type:Boolean,default:!0},editLabel:{},callbacks:{},defaultValues:{},values:{},strings:{},reducedMotion:{type:Boolean}},setup(l){const t=l,n=e.computed(()=>t.fields?t.fields:t.formConfig?t.formConfig.fields:[]),o=e.computed(()=>{if(t.welcome)return t.welcome;if(t.formConfig&&"welcome"in t.formConfig)return t.formConfig.welcome}),u=e.computed(()=>{if(t.done)return t.done;if(t.formConfig&&"done"in t.formConfig)return t.formConfig.done}),p=e.computed(()=>{var d;return{...t.typewriter,enabled:t.reducedMotion?!1:((d=t.typewriter)==null?void 0:d.enabled)??!0}}),m=e.computed(()=>C.mergeStrings(t.strings)),{snapshot:s,startTyping:g,activateField:w,confirmField:E,editField:V,reconfirmField:f,getMeta:i}=I(n.value),F=e.computed(()=>{var d;return((d=o.value)==null?void 0:d.show)!==!1&&o.value!==void 0}),b=e.ref(!F.value),N=e.ref(!1),x=()=>{if(!b.value||N.value||n.value.length===0)return;N.value=!0;const d=n.value.find(r=>s.value.statuses[r.key]!=="confirmed");d&&g(d.key)};e.watch(b,x),e.onMounted(x);const T=(d,r)=>{var v;s.value.statuses[d]==="editing"?f(d,r):E(d,r),(v=t.callbacks)!=null&&v.onFieldComplete&&t.callbacks.onFieldComplete(d,r,0);const S=n.value.findIndex(c=>c.key===d);for(let c=S+1;c<n.value.length;c++){const a=n.value[c];if(!(!a||s.value.statuses[a.key]==="confirmed")){g(a.key);break}}},$=e.computed(()=>n.value.filter(d=>{const r=s.value.statuses[d.key];return r==="typing"||r==="active"||r==="confirmed"||r==="editing"})),h=e.ref(null);return e.watch(()=>$.value.length,()=>{e.nextTick(()=>{h.value&&(h.value.scrollTop=h.value.scrollHeight)})}),(d,r)=>(e.openBlock(),e.createElementBlock("div",{class:"ns-root",ref_key:"scrollContainerRef",ref:h},[!b.value&&o.value?(e.openBlock(),e.createBlock(D,{key:0,welcome:o.value,typewriter:p.value,onStart:r[0]||(r[0]=k=>b.value=!0)},null,8,["welcome","typewriter"])):e.createCommentVNode("",!0),b.value?(e.openBlock(),e.createElementBlock("div",ee,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList($.value,k=>{var S;return e.openBlock(),e.createBlock(L,{key:k.key,field:k,status:e.unref(s).statuses[k.key]??"idle",value:((S=t.values)==null?void 0:S[k.key])??e.unref(s).values[k.key],allValues:e.unref(s).values,typewriter:p.value,editable:t.editable,locked:!1,editLabel:t.editLabel??m.value.editLabel,onTypingComplete:e.unref(w),onConfirm:T,onEdit:r[1]||(r[1]=v=>{var c,a;e.unref(V)(v),(a=(c=t.callbacks)==null?void 0:c.onEdit)==null||a.call(c,v)}),onError:r[2]||(r[2]=(v,c)=>{var a,y;return(y=(a=t.callbacks)==null?void 0:a.onError)==null?void 0:y.call(a,v,c)}),onChange:r[3]||(r[3]=(v,c)=>{var a,y;return(y=(a=t.callbacks)==null?void 0:a.onChange)==null?void 0:y.call(a,v,c)}),onFocus:r[4]||(r[4]=v=>{var c,a;return(a=(c=t.callbacks)==null?void 0:c.onFieldFocus)==null?void 0:a.call(c,v)}),onBlur:r[5]||(r[5]=(v,c)=>{var a,y;return(y=(a=t.callbacks)==null?void 0:a.onFieldBlur)==null?void 0:y.call(a,v,c)})},null,8,["field","status","value","allValues","typewriter","editable","editLabel","onTypingComplete"])}),128))])):e.createCommentVNode("",!0),e.unref(s).isComplete&&u.value?(e.openBlock(),e.createBlock(M,{key:2,done:u.value,values:e.unref(s).values,meta:e.unref(i)(),typewriter:p.value},null,8,["done","values","meta","typewriter"])):e.createCommentVNode("",!0)],512))}}),ne=B(te,[["__scopeId","data-v-33050240"]]);exports.DoneScreen=M;exports.ErrorMessage=_;exports.Line=L;exports.NarrativeForm=ne;exports.WelcomeScreen=D;exports.useFormState=I;
@@ -0,0 +1,7 @@
1
+ export { default as NarrativeForm } from './components/NarrativeForm.vue';
2
+ export { default as WelcomeScreen } from './components/WelcomeScreen.vue';
3
+ export { default as DoneScreen } from './components/DoneScreen.vue';
4
+ export { default as Line } from './components/Line.vue';
5
+ export { default as ErrorMessage } from './components/ErrorMessage.vue';
6
+ export * from './composables/useFormState';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,+BAA+B,CAAC;AACxE,cAAc,4BAA4B,CAAC"}
package/dist/index.mjs ADDED
@@ -0,0 +1,319 @@
1
+ import { triggerRef as U, shallowRef as j, onUnmounted as N, defineComponent as _, ref as g, onMounted as H, openBlock as f, createElementBlock as y, createElementVNode as $, toDisplayString as k, createCommentVNode as F, createBlock as V, Transition as q, withCtx as z, watch as E, nextTick as O, withKeys as G, withModifiers as J, createVNode as Q, computed as x, Fragment as X, renderList as Y, unref as b } from "vue";
2
+ import { FormStateEngine as Z, validateField as ee, hasAsyncValidation as te, validateFieldAsync as ne, mergeStrings as le } from "@viveksinghind/narrative-form-core";
3
+ function se(n) {
4
+ const e = new Z(n, () => {
5
+ t.value = e.getSnapshot(), U(t);
6
+ }), t = j(e.getSnapshot());
7
+ return N(() => {
8
+ e.reset();
9
+ }), {
10
+ snapshot: t,
11
+ startTyping: (l) => e.startTyping(l),
12
+ activateField: (l) => e.activateField(l),
13
+ confirmField: (l, u) => e.confirmField(l, u),
14
+ editField: (l) => e.editField(l),
15
+ reconfirmField: (l, u) => e.reconfirmField(l, u),
16
+ next: () => e.next(),
17
+ focusField: (l) => e.focusField(l),
18
+ reset: () => {
19
+ e.reset(), t.value = e.getSnapshot();
20
+ },
21
+ getValues: () => e.getValues(),
22
+ getMeta: (l, u) => e.getMeta(l, u)
23
+ };
24
+ }
25
+ const ae = { class: "ns-welcome fade-in" }, ie = { class: "ns-welcome-title" }, oe = {
26
+ key: 0,
27
+ class: "ns-welcome-subtitle"
28
+ }, re = /* @__PURE__ */ _({
29
+ __name: "WelcomeScreen",
30
+ props: {
31
+ welcome: {},
32
+ typewriter: {}
33
+ },
34
+ emits: ["start"],
35
+ setup(n, { emit: e }) {
36
+ const t = n, l = e, u = g("");
37
+ let p;
38
+ return H(() => {
39
+ if (!t.typewriter.enabled) {
40
+ u.value = t.welcome.heading || "";
41
+ return;
42
+ }
43
+ let c = 0;
44
+ p = setInterval(() => {
45
+ u.value = (t.welcome.heading || "").slice(0, c + 1), c++, c >= (t.welcome.heading || "").length && clearInterval(p);
46
+ }, t.typewriter.speed ?? 30);
47
+ }), N(() => {
48
+ clearInterval(p);
49
+ }), (c, i) => (f(), y("div", ae, [
50
+ $("h1", ie, k(u.value), 1),
51
+ n.welcome.subtext ? (f(), y("p", oe, k(n.welcome.subtext), 1)) : F("", !0),
52
+ $("button", {
53
+ type: "button",
54
+ class: "ns-welcome-start",
55
+ onClick: i[0] || (i[0] = (h) => l("start"))
56
+ }, k(n.welcome.ctaLabel || "Start"), 1)
57
+ ]));
58
+ }
59
+ }), L = (n, e) => {
60
+ const t = n.__vccOpts || n;
61
+ for (const [l, u] of e)
62
+ t[l] = u;
63
+ return t;
64
+ }, ue = /* @__PURE__ */ L(re, [["__scopeId", "data-v-dbeb46ea"]]), de = { class: "ns-done fade-in" }, ce = { class: "ns-done-title" }, ve = /* @__PURE__ */ _({
65
+ __name: "DoneScreen",
66
+ props: {
67
+ done: {},
68
+ values: {},
69
+ meta: {},
70
+ typewriter: {}
71
+ },
72
+ setup(n) {
73
+ return (e, t) => (f(), y("div", de, [
74
+ $("h1", ce, k(typeof n.done.message == "function" ? n.done.message(n.values) : n.done.message), 1)
75
+ ]));
76
+ }
77
+ }), fe = /* @__PURE__ */ L(ve, [["__scopeId", "data-v-7802c8f5"]]), me = {
78
+ key: 0,
79
+ class: "ns-error-wrapper"
80
+ }, pe = { class: "ns-error-text" }, ye = /* @__PURE__ */ _({
81
+ __name: "ErrorMessage",
82
+ props: {
83
+ message: {},
84
+ config: {}
85
+ },
86
+ setup(n) {
87
+ return (e, t) => (f(), V(q, { name: "fade-slide" }, {
88
+ default: z(() => [
89
+ n.message ? (f(), y("div", me, [
90
+ $("p", pe, k(n.message), 1)
91
+ ])) : F("", !0)
92
+ ]),
93
+ _: 1
94
+ }));
95
+ }
96
+ }), ge = /* @__PURE__ */ L(ye, [["__scopeId", "data-v-a8be2d7b"]]), we = { class: "ns-line fade-in" }, be = { class: "ns-prompt-row" }, ke = { class: "ns-prompt" }, Fe = {
97
+ key: 0,
98
+ class: "ns-value"
99
+ }, he = {
100
+ key: 0,
101
+ class: "ns-input-row"
102
+ }, Ce = ["type", "disabled", "value", "onKeydown"], xe = /* @__PURE__ */ _({
103
+ __name: "Line",
104
+ props: {
105
+ field: {},
106
+ status: {},
107
+ value: {},
108
+ allValues: {},
109
+ typewriter: {},
110
+ editable: { type: Boolean },
111
+ locked: { type: Boolean },
112
+ editLabel: {}
113
+ },
114
+ emits: ["typingComplete", "confirm", "edit", "error", "change", "focus", "blur"],
115
+ setup(n, { emit: e }) {
116
+ const t = n, l = e, u = g(null), p = g(""), c = g(t.value ? String(t.value) : ""), i = g(null), h = g(!1);
117
+ let I;
118
+ E(() => t.value, (v) => {
119
+ v !== void 0 && String(v) !== c.value && (c.value = String(v));
120
+ }), E(() => t.status, (v) => {
121
+ if (v === "typing") {
122
+ if (!t.typewriter.enabled) {
123
+ p.value = t.field.prompt, l("typingComplete", t.field.key);
124
+ return;
125
+ }
126
+ let o = 0;
127
+ I = setInterval(() => {
128
+ p.value = t.field.prompt.slice(0, o + 1), o++, o >= t.field.prompt.length && (clearInterval(I), l("typingComplete", t.field.key));
129
+ }, t.typewriter.speed ?? 30);
130
+ } else (v === "active" || v === "editing") && O(() => {
131
+ var o;
132
+ (o = u.value) == null || o.focus();
133
+ });
134
+ }, { immediate: !0 }), N(() => {
135
+ clearInterval(I);
136
+ });
137
+ const R = (v) => {
138
+ const o = v.target;
139
+ c.value = o.value, l("change", t.field.key, o.value), i.value && (i.value = null);
140
+ }, D = async () => {
141
+ const v = ee(t.field, c.value);
142
+ if (!v.valid) {
143
+ i.value = v.error || "Invalid input", l("error", t.field.key, i.value);
144
+ return;
145
+ }
146
+ if (te(t.field)) {
147
+ h.value = !0;
148
+ const o = await ne(t.field, c.value);
149
+ if (h.value = !1, !o.valid) {
150
+ i.value = o.error || "Invalid input", l("error", t.field.key, i.value);
151
+ return;
152
+ }
153
+ }
154
+ i.value = null, l("confirm", t.field.key, c.value);
155
+ };
156
+ return (v, o) => (f(), y("div", we, [
157
+ $("div", be, [
158
+ $("span", ke, k(n.status === "typing" ? p.value : n.field.prompt), 1),
159
+ n.status === "confirmed" ? (f(), y("span", Fe, k(c.value), 1)) : F("", !0),
160
+ n.status === "confirmed" && n.editable && !n.locked ? (f(), y("button", {
161
+ key: 1,
162
+ type: "button",
163
+ class: "ns-edit-btn",
164
+ onClick: o[0] || (o[0] = (B) => l("edit", n.field.key))
165
+ }, k(n.editLabel), 1)) : F("", !0)
166
+ ]),
167
+ n.status === "active" || n.status === "editing" ? (f(), y("div", he, [
168
+ $("input", {
169
+ ref_key: "inputRef",
170
+ ref: u,
171
+ type: n.field.type === "email" ? "email" : n.field.type === "password" ? "password" : "text",
172
+ class: "ns-input",
173
+ disabled: h.value,
174
+ value: c.value,
175
+ onInput: R,
176
+ onFocus: o[1] || (o[1] = (B) => l("focus", n.field.key)),
177
+ onBlur: o[2] || (o[2] = (B) => l("blur", n.field.key, c.value)),
178
+ onKeydown: G(J(D, ["prevent"]), ["enter"])
179
+ }, null, 40, Ce)
180
+ ])) : F("", !0),
181
+ Q(ge, { message: i.value }, null, 8, ["message"])
182
+ ]));
183
+ }
184
+ }), $e = /* @__PURE__ */ L(xe, [["__scopeId", "data-v-d4b482a6"]]), Ie = {
185
+ key: 1,
186
+ class: "ns-form-body"
187
+ }, Se = /* @__PURE__ */ _({
188
+ __name: "NarrativeForm",
189
+ props: {
190
+ fields: {},
191
+ formConfig: {},
192
+ typewriter: {},
193
+ welcome: {},
194
+ done: {},
195
+ editable: { type: Boolean, default: !0 },
196
+ editLabel: {},
197
+ callbacks: {},
198
+ defaultValues: {},
199
+ values: {},
200
+ strings: {},
201
+ reducedMotion: { type: Boolean }
202
+ },
203
+ setup(n) {
204
+ const e = n, t = x(() => e.fields ? e.fields : e.formConfig ? e.formConfig.fields : []), l = x(() => {
205
+ if (e.welcome) return e.welcome;
206
+ if (e.formConfig && "welcome" in e.formConfig) return e.formConfig.welcome;
207
+ }), u = x(() => {
208
+ if (e.done) return e.done;
209
+ if (e.formConfig && "done" in e.formConfig) return e.formConfig.done;
210
+ }), p = x(() => {
211
+ var d;
212
+ return {
213
+ ...e.typewriter,
214
+ enabled: e.reducedMotion ? !1 : ((d = e.typewriter) == null ? void 0 : d.enabled) ?? !0
215
+ };
216
+ }), c = x(() => le(e.strings)), {
217
+ snapshot: i,
218
+ startTyping: h,
219
+ activateField: I,
220
+ confirmField: R,
221
+ editField: D,
222
+ reconfirmField: v,
223
+ getMeta: o
224
+ } = se(t.value), B = x(() => {
225
+ var d;
226
+ return ((d = l.value) == null ? void 0 : d.show) !== !1 && l.value !== void 0;
227
+ }), S = g(!B.value), W = g(!1), K = () => {
228
+ if (!S.value || W.value || t.value.length === 0) return;
229
+ W.value = !0;
230
+ const d = t.value.find((a) => i.value.statuses[a.key] !== "confirmed");
231
+ d && h(d.key);
232
+ };
233
+ E(S, K), H(K);
234
+ const P = (d, a) => {
235
+ var m;
236
+ i.value.statuses[d] === "editing" ? v(d, a) : R(d, a), (m = e.callbacks) != null && m.onFieldComplete && e.callbacks.onFieldComplete(d, a, 0);
237
+ const T = t.value.findIndex((r) => r.key === d);
238
+ for (let r = T + 1; r < t.value.length; r++) {
239
+ const s = t.value[r];
240
+ if (!(!s || i.value.statuses[s.key] === "confirmed")) {
241
+ h(s.key);
242
+ break;
243
+ }
244
+ }
245
+ }, A = x(() => t.value.filter((d) => {
246
+ const a = i.value.statuses[d.key];
247
+ return a === "typing" || a === "active" || a === "confirmed" || a === "editing";
248
+ })), M = g(null);
249
+ return E(() => A.value.length, () => {
250
+ O(() => {
251
+ M.value && (M.value.scrollTop = M.value.scrollHeight);
252
+ });
253
+ }), (d, a) => (f(), y("div", {
254
+ class: "ns-root",
255
+ ref_key: "scrollContainerRef",
256
+ ref: M
257
+ }, [
258
+ !S.value && l.value ? (f(), V(ue, {
259
+ key: 0,
260
+ welcome: l.value,
261
+ typewriter: p.value,
262
+ onStart: a[0] || (a[0] = (C) => S.value = !0)
263
+ }, null, 8, ["welcome", "typewriter"])) : F("", !0),
264
+ S.value ? (f(), y("div", Ie, [
265
+ (f(!0), y(X, null, Y(A.value, (C) => {
266
+ var T;
267
+ return f(), V($e, {
268
+ key: C.key,
269
+ field: C,
270
+ status: b(i).statuses[C.key] ?? "idle",
271
+ value: ((T = e.values) == null ? void 0 : T[C.key]) ?? b(i).values[C.key],
272
+ allValues: b(i).values,
273
+ typewriter: p.value,
274
+ editable: e.editable,
275
+ locked: !1,
276
+ editLabel: e.editLabel ?? c.value.editLabel,
277
+ onTypingComplete: b(I),
278
+ onConfirm: P,
279
+ onEdit: a[1] || (a[1] = (m) => {
280
+ var r, s;
281
+ b(D)(m), (s = (r = e.callbacks) == null ? void 0 : r.onEdit) == null || s.call(r, m);
282
+ }),
283
+ onError: a[2] || (a[2] = (m, r) => {
284
+ var s, w;
285
+ return (w = (s = e.callbacks) == null ? void 0 : s.onError) == null ? void 0 : w.call(s, m, r);
286
+ }),
287
+ onChange: a[3] || (a[3] = (m, r) => {
288
+ var s, w;
289
+ return (w = (s = e.callbacks) == null ? void 0 : s.onChange) == null ? void 0 : w.call(s, m, r);
290
+ }),
291
+ onFocus: a[4] || (a[4] = (m) => {
292
+ var r, s;
293
+ return (s = (r = e.callbacks) == null ? void 0 : r.onFieldFocus) == null ? void 0 : s.call(r, m);
294
+ }),
295
+ onBlur: a[5] || (a[5] = (m, r) => {
296
+ var s, w;
297
+ return (w = (s = e.callbacks) == null ? void 0 : s.onFieldBlur) == null ? void 0 : w.call(s, m, r);
298
+ })
299
+ }, null, 8, ["field", "status", "value", "allValues", "typewriter", "editable", "editLabel", "onTypingComplete"]);
300
+ }), 128))
301
+ ])) : F("", !0),
302
+ b(i).isComplete && u.value ? (f(), V(fe, {
303
+ key: 2,
304
+ done: u.value,
305
+ values: b(i).values,
306
+ meta: b(o)(),
307
+ typewriter: p.value
308
+ }, null, 8, ["done", "values", "meta", "typewriter"])) : F("", !0)
309
+ ], 512));
310
+ }
311
+ }), Be = /* @__PURE__ */ L(Se, [["__scopeId", "data-v-33050240"]]);
312
+ export {
313
+ fe as DoneScreen,
314
+ ge as ErrorMessage,
315
+ $e as Line,
316
+ Be as NarrativeForm,
317
+ ue as WelcomeScreen,
318
+ se as useFormState
319
+ };
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .fade-in[data-v-dbeb46ea]{animation:fadeIn-dbeb46ea .5s ease-out}@keyframes fadeIn-dbeb46ea{0%{opacity:0}to{opacity:1}}.ns-welcome[data-v-dbeb46ea]{padding:2rem}.ns-welcome-title[data-v-dbeb46ea]{font-size:2rem;font-weight:700;margin-bottom:.5rem}.ns-welcome-subtitle[data-v-dbeb46ea]{font-size:1.125rem;opacity:.8;margin-bottom:2rem}.ns-welcome-start[data-v-dbeb46ea]{padding:.75rem 1.5rem;background-color:#007bff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:1rem}.fade-in[data-v-7802c8f5]{animation:fadeIn-7802c8f5 .5s ease-out}@keyframes fadeIn-7802c8f5{0%{opacity:0}to{opacity:1}}.ns-done[data-v-7802c8f5]{padding:2rem}.ns-done-title[data-v-7802c8f5]{font-size:1.5rem;font-weight:700;margin-bottom:.5rem}.ns-done-subtitle[data-v-7802c8f5]{font-size:1rem;opacity:.8}.fade-slide-enter-active[data-v-a8be2d7b],.fade-slide-leave-active[data-v-a8be2d7b]{transition:opacity .2s,transform .2s}.fade-slide-enter-from[data-v-a8be2d7b],.fade-slide-leave-to[data-v-a8be2d7b]{opacity:0;transform:translateY(-10px)}.ns-error-text[data-v-a8be2d7b]{color:#d32f2f;font-size:.875rem;margin-top:4px}.fade-in[data-v-d4b482a6]{animation:fadeIn-d4b482a6 .3s ease-out}@keyframes fadeIn-d4b482a6{0%{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}.ns-line[data-v-d4b482a6]{margin:1rem 0}.ns-prompt-row[data-v-d4b482a6]{display:flex;align-items:center;flex-wrap:wrap}.ns-prompt[data-v-d4b482a6]{font-size:1.125rem;margin-right:.5rem}.ns-value[data-v-d4b482a6]{font-size:1.125rem;font-weight:600;color:#007bff;margin-right:.5rem}.ns-edit-btn[data-v-d4b482a6]{margin-left:.5rem;padding:.25rem .5rem;border-radius:4px;background-color:#0000000d;border:none;cursor:pointer;font-size:.75rem;font-weight:700}.ns-input-row[data-v-d4b482a6]{margin-top:.5rem}.ns-input[data-v-d4b482a6]{font-size:1.125rem;padding:.5rem 0;border:none;border-bottom:2px solid #007bff;outline:none;width:100%;background:transparent}.ns-input[data-v-d4b482a6]:disabled{border-bottom-color:#ccc}.ns-root[data-v-33050240]{display:flex;flex-direction:column;height:100%;overflow-y:auto;padding:1.25rem}.ns-form-body[data-v-33050240]{flex-grow:1}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@viveksinghind/narrative-form-vue",
3
+ "version": "1.0.2",
4
+ "description": "Vue 3 components for narrative-form — typewriter-style sign-up flow",
5
+ "author": "Vivek Singh <vivekSinghInd>",
6
+ "license": "MIT",
7
+ "main": "dist/index.cjs",
8
+ "module": "dist/index.mjs",
9
+ "types": "dist/index.d.ts",
10
+ "sideEffects": false,
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "import": "./dist/index.mjs",
17
+ "require": "./dist/index.cjs",
18
+ "types": "./dist/index.d.ts"
19
+ }
20
+ },
21
+ "dependencies": {
22
+ "@viveksinghind/narrative-form-core": "1.0.2"
23
+ },
24
+ "peerDependencies": {
25
+ "vue": ">=3.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@vitejs/plugin-vue": "^5.0.4",
29
+ "rimraf": "^5.0.5",
30
+ "typescript": "^5.4.5",
31
+ "vite": "^5.2.11",
32
+ "vite-plugin-dts": "^5.0.3",
33
+ "vue": "^3.4.27",
34
+ "vue-tsc": "^2.0.16"
35
+ },
36
+ "keywords": [
37
+ "vue",
38
+ "vue3",
39
+ "narrative-form",
40
+ "typewriter",
41
+ "form",
42
+ "onboarding",
43
+ "signup",
44
+ "typescript"
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/vivekSinghInd/narrative-form.git",
49
+ "directory": "packages/vue"
50
+ },
51
+ "scripts": {
52
+ "build": "vite build",
53
+ "dev": "vite build --watch",
54
+ "typecheck": "vue-tsc --noEmit",
55
+ "clean": "rimraf dist"
56
+ }
57
+ }