business-obligations-ui 0.2.1 → 0.4.0
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/README.md +107 -0
- package/dist/index.cjs +1961 -261
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +384 -6
- package/dist/index.d.ts +384 -6
- package/dist/index.js +1923 -259
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -12,6 +12,113 @@ import { planLaunch, evaluate } from "business-obligations";
|
|
|
12
12
|
|
|
13
13
|
The components are controlled: the host owns completed steps and obligation filters.
|
|
14
14
|
|
|
15
|
+
## The whole form: `ProfileForm`
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { ProfileForm, useProfileChoices, lookupPlace, profileFromChoices } from "business-obligations-ui";
|
|
19
|
+
|
|
20
|
+
<ProfileForm /> // everything a person can tell about the business
|
|
21
|
+
<ProfileForm values={{ zip: "85234", entityType: "llc" }} /> // what the host already knows is not asked again
|
|
22
|
+
|
|
23
|
+
const [choices] = useProfileChoices(); // the answers, read from the address
|
|
24
|
+
const place = await lookupPlace(choices.zip); // { city, county, state }
|
|
25
|
+
const profile = profileFromChoices(choices, place!); // a `BusinessProfile` for `evaluate` / `planLaunch`
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It asks for the ZIP code (with the place it stands for), type, industry, profession (when it applies), people, money,
|
|
29
|
+
tax details, the yes/no details, and the dates of the plan, in groups. Nothing is submitted: every answer goes into the
|
|
30
|
+
address, so a link shares the form, and a host that reads `useProfileChoices()` sees each change.
|
|
31
|
+
|
|
32
|
+
**Values the host passes are not asked.** Each value in `values` replaces what the address says, `useProfileChoices`
|
|
33
|
+
returns it, and its field (and its heading, when all of its fields are gone) is not shown. To read the same values
|
|
34
|
+
outside the form, give them to `ProfileQueryProvider values={...}`, which the form picks up. The single fields
|
|
35
|
+
(`TypeField`, `IndustryField`, `EmployeesField`, `DetailsField`) follow the same rule.
|
|
36
|
+
|
|
37
|
+
**Where the business is** can be passed as a `zip`, or as `city`, `county` and `state` (the ZIP field is then hidden too, even when only some of the three are given).
|
|
38
|
+
`useProfilePlace()` gives the `Place` for the engine either way: what the host passed, with the rest looked up from the
|
|
39
|
+
ZIP code (`undefined` while it is not known yet, `null` for a ZIP code that does not exist).
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<ProfileForm values={{ city: "Gilbert", county: "Maricopa", state: "AZ" }} />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Old links keep working: `entity=`, `goods=`, `dba=` and `industry=restaurant` are read, and not written again.
|
|
46
|
+
|
|
47
|
+
### How the package is laid out
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
src/index.ts the public API: everything is exported by name from here
|
|
51
|
+
src/i18n/ messages (en, es), the one set of formatters (money, dates, relative days), the locale context
|
|
52
|
+
src/shared/ styles of the controls, the small pieces the lists are made of, URL checks
|
|
53
|
+
src/plan/ LaunchList, ObligationsList, the item cards and filters
|
|
54
|
+
src/profile/choices.ts every choice of the profile, described once: its type, default, URL parameter, reading, writing
|
|
55
|
+
src/profile/store.tsx the address as state: provider, hooks, `setProfileQuery`
|
|
56
|
+
src/profile/place.ts ZIP code to place, and choices to a `BusinessProfile`
|
|
57
|
+
src/profile/fields/ one file per field; ProfileForm.tsx puts them together
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`choices.ts` is the single source for the profile: the `ProfileChoices` type, `PROFILE_DEFAULTS`, `PROFILE_PARAMS`, the
|
|
61
|
+
lists of details and numbers, and reading and writing the address are all derived from one table, so adding a choice
|
|
62
|
+
means adding one line there (and its field). The formatters in `i18n/format.ts` are exported, so a host formats
|
|
63
|
+
money and dates the same way the lists do.
|
|
64
|
+
|
|
65
|
+
## Business details in the URL
|
|
66
|
+
|
|
67
|
+
Fields collect the details the engine needs, and keep them in the address bar, so a link shares the whole setup:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { TypeField, IndustryField, EmployeesField, DetailsField, useProfileChoices } from "business-obligations-ui";
|
|
71
|
+
|
|
72
|
+
<TypeField /> {/* ?type=pllc */}
|
|
73
|
+
<IndustryField /> {/* ?naics=621111 */}
|
|
74
|
+
<EmployeesField /> {/* ?employees=12 */}
|
|
75
|
+
<DetailsField /> {/* ?sells=1&home=1&tradeName=1&exempt=1&signage=1&food=1 */}
|
|
76
|
+
|
|
77
|
+
const [choices] = useProfileChoices(); // { entityType, naics, employeeCount, sellsTaxableGoods, ... }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- `IndustryField` takes a NAICS code, or searches the code list by word and fills in the code of the result. The list
|
|
81
|
+
(`business-obligations/naics`) is a separate bundle, loaded when the person starts typing.
|
|
82
|
+
- The options come from the engine (`ENTITY_TYPES`), so a new type there shows up here.
|
|
83
|
+
- The inputs own the query string. A change goes in with `history.replaceState` (no page load, no history entry), keeps
|
|
84
|
+
the other parameters and the hash, and updates every input and hook on the page. A value equal to its default is left
|
|
85
|
+
out, and an invalid or missing one falls back to the default.
|
|
86
|
+
- `DetailsField` shows a three-state checkbox for every yes/no detail except "not formed yet": sells taxable goods, run
|
|
87
|
+
from home, trade name, tax-exempt status, exterior signage and food service. The S election appears for professional
|
|
88
|
+
and benefit corporations. Each is **yes** (✓, `?sells=1`), **no** (✕, `?sells=0`) or **not answered yet** (a dash with
|
|
89
|
+
a dashed border, no parameter, the checkbox's "indeterminate" state); pressing it goes round in that order, and the
|
|
90
|
+
answer is written beside it. In `ProfileChoices` they are `true`, `false` and `undefined`, and `undefined` is what
|
|
91
|
+
the engine treats as unknown. `legend={false}` hides the group title visually when the host has its own heading.
|
|
92
|
+
- `useProfileChoices()` returns the current choices and a setter. Use it to build the plan:
|
|
93
|
+
`planLaunch({ ...place, ...choices })`.
|
|
94
|
+
- To preselect something, wrap the inputs in `<ProfileQueryProvider defaults={{ naics: "722", employeeCount: 3 }}>`.
|
|
95
|
+
(A value the host will not let the person change is `values`, above.) `readProfileQuery`, `writeProfileQuery`, `setProfileQuery`, `PROFILE_PARAMS` and `PROFILE_DEFAULTS` are exported for
|
|
96
|
+
server code and links.
|
|
97
|
+
- Nothing is chosen by default: `choices.entityType` is `undefined` until a type is picked (the engine needs one, so
|
|
98
|
+
decide what to analyze when it is missing) and `employeeCount` is `undefined` until a number is typed. Every field
|
|
99
|
+
except the checkboxes has a ✕ that empties it. Pass `defaults` to the provider to preselect something.
|
|
100
|
+
- Typed values are checked with [zod](https://zod.dev) (`profile/validation.ts`): a head count must be a whole number from 0 to
|
|
101
|
+
10,000, and a NAICS code needs 2 to 6 digits and must be in the code list. A field with a problem gets a red border
|
|
102
|
+
and a message from `messages.<locale>.profile.errors`; a bad head count is kept in the box but not written to the
|
|
103
|
+
address. Choosing a business type is optional, so an empty one is never an error.
|
|
104
|
+
- On the server, and while hydrating, the inputs show the defaults; they pick up the URL right after.
|
|
105
|
+
- Labels follow the `locale` prop like the lists do (`<TypeField locale="es" />`), from `messages.<locale>.profile`.
|
|
106
|
+
|
|
107
|
+
## Conditionally applicable obligations
|
|
108
|
+
|
|
109
|
+
`ObligationsList` takes an optional `conditional` list and shows it in its own section after the others: items that are
|
|
110
|
+
not in `items` now, but would be if a detail that was left blank turned out a certain way. Each is a `ConditionalItem`,
|
|
111
|
+
`{ item, when: [{ field, values }] }`, and its card says what would bring it in ("Applies if number of employees: 1, 2,
|
|
112
|
+
3 …"). The package does not compute them; the playground does, from `analyzeInputs` and `evaluate`
|
|
113
|
+
(`conditionalItems` in `apps/web/src/lib/plan.ts`). The list's filters apply to the section too.
|
|
114
|
+
|
|
115
|
+
## Bundle size
|
|
116
|
+
|
|
117
|
+
The package is ESM with `sideEffects: false`, and nothing runs when a module loads, so a bundler keeps only what an app
|
|
118
|
+
imports: `LaunchList` alone is about 17 KB minified (the peer dependencies are not counted), and zod comes along only with
|
|
119
|
+
`EmployeesField` and `IndustryField`, which validate. `src/treeshake.test.ts` bundles small consumers to keep it so.
|
|
120
|
+
Keep new top-level calls out of the modules (or mark them `/* @__PURE__ */`).
|
|
121
|
+
|
|
15
122
|
## Languages
|
|
16
123
|
|
|
17
124
|
English is the default and Spanish ships with the package. Two things are translated, by two packages:
|