@etamong-playground/legal 0.3.1
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 +21 -0
- package/README.md +104 -0
- package/dist/chunk-MDFCRQ5H.js +22 -0
- package/dist/helpers-CRGM1DHb.d.cts +152 -0
- package/dist/helpers-CRGM1DHb.d.ts +152 -0
- package/dist/helpers.cjs +27 -0
- package/dist/helpers.d.cts +2 -0
- package/dist/helpers.d.ts +2 -0
- package/dist/helpers.js +1 -0
- package/dist/index.cjs +195 -0
- package/dist/index.d.cts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +166 -0
- package/dist/styles.css +191 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 etamong
|
|
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,104 @@
|
|
|
1
|
+
# @etamong-playground/legal
|
|
2
|
+
|
|
3
|
+
> **About** — One of several small shared libraries used across a personal "fleet" of small apps (error handling · audit logging · encryption-at-rest · i18n · UI · …). Authored and maintained with [Claude Code](https://www.anthropic.com/claude-code) (Anthropic's agentic CLI). Each README documents the design rationale behind the library.
|
|
4
|
+
>
|
|
5
|
+
> **This is a public repository** — keep internal infrastructure details (hostnames, secret/Vault paths, private URLs, internal issue/MR references) out of code, comments, and commit messages.
|
|
6
|
+
|
|
7
|
+
Shared, versioned legal-document toolkit for etamong-playground apps — one model,
|
|
8
|
+
renderers, and advance-notice logic for Terms (이용약관) / Privacy (개인정보처리방침),
|
|
9
|
+
so each app stops re-implementing its own.
|
|
10
|
+
|
|
11
|
+
**Headless & styleless.** Ships a model + helpers + renderers that emit
|
|
12
|
+
`legal-*` class names only. Theme via the optional stylesheet's CSS variables,
|
|
13
|
+
or style the classes yourself. No router, no bundled content — each app owns its
|
|
14
|
+
own version data in its own repo (so the legal audit trail stays in git).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pnpm add @etamong-playground/legal
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Resolving `@etamong-playground/*` from GitHub Packages requires the registry in `.npmrc`:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
@etamong-playground:registry=https://npm.pkg.github.com
|
|
26
|
+
//npm.pkg.github.com/:_authToken=<your-github-token>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Model
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
interface LegalVersion {
|
|
33
|
+
version: string; // "1.0"
|
|
34
|
+
publishedDate: string; // 공고일 YYYY-MM-DD
|
|
35
|
+
effectiveDate: string; // 시행일 YYYY-MM-DD
|
|
36
|
+
summary?: string;
|
|
37
|
+
adverse?: boolean; // 불리/중대 → 30일 사전공지 (else 7일)
|
|
38
|
+
sections: { title: string; content: ReactNode }[];
|
|
39
|
+
changes?: VersionChange[]; // diff vs previous version
|
|
40
|
+
}
|
|
41
|
+
interface LegalDocMeta { kind: "terms" | "privacy"; title: string; versions: LegalVersion[] }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Newest version first. Publishing a change = append a version with a future
|
|
45
|
+
`effectiveDate`. Advance-notice (공시) and the effective-date flip are pure date
|
|
46
|
+
functions — no scheduled job.
|
|
47
|
+
|
|
48
|
+
## Helpers
|
|
49
|
+
|
|
50
|
+
- `effectiveVersion(versions, today?)` — the version in force.
|
|
51
|
+
- `upcomingVersion(versions, today?)` — soonest announced-but-not-yet-effective
|
|
52
|
+
version, or null. Guarded by `publishedDate <= today` so drafts don't trigger
|
|
53
|
+
a public banner.
|
|
54
|
+
- `todayISO(timeZone = "Asia/Seoul")` — today as YYYY-MM-DD in the jurisdiction.
|
|
55
|
+
|
|
56
|
+
## Components
|
|
57
|
+
|
|
58
|
+
| Component | Purpose |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `<LegalDocument version>` | Renders a version's titled sections. |
|
|
61
|
+
| `<VersionDiff changes fromVersion toVersion>` | 3-column 변경 전/후 table; `==highlight==` markup. |
|
|
62
|
+
| `<VersionHistory versions selectedIdx onSelect>` | Controlled version picker. |
|
|
63
|
+
| `<UpcomingNotice docTitle version onPreview?>` | Per-doc 변경 예정 card. |
|
|
64
|
+
| `<PolicyNotice docs hrefFor renderLink?>` | Site-wide dismissible 사전공시 banner. |
|
|
65
|
+
|
|
66
|
+
`PolicyNotice` is router-agnostic — pass `renderLink={(href, kids) => <Link to={href}>{kids}</Link>}`
|
|
67
|
+
to use your app's Link. Interactive components use state; mount them inside a
|
|
68
|
+
client boundary (Next.js `"use client"`).
|
|
69
|
+
|
|
70
|
+
## Theming
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import "@etamong-playground/legal/styles.css";
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Override CSS variables for theme / dark mode:
|
|
77
|
+
|
|
78
|
+
```css
|
|
79
|
+
.dark {
|
|
80
|
+
--legal-fg: #d4d4d8;
|
|
81
|
+
--legal-border: #3f3f46;
|
|
82
|
+
--legal-surface: #27272a;
|
|
83
|
+
--legal-notice-bg: #1e3a8a33;
|
|
84
|
+
/* ... see src/styles.css for the full list */
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Releasing
|
|
89
|
+
|
|
90
|
+
Bump `version` in `package.json`, commit, then push a matching tag:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
git tag v0.1.0 && git push origin v0.1.0
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
CI publishes to GitHub Packages on tags matching `vX.Y.Z`.
|
|
97
|
+
|
|
98
|
+
## Acknowledgements
|
|
99
|
+
|
|
100
|
+
Built for [React](https://react.dev) (peer dependency, MIT).
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/helpers.ts
|
|
2
|
+
function todayISO(timeZone = "Asia/Seoul") {
|
|
3
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
4
|
+
timeZone,
|
|
5
|
+
year: "numeric",
|
|
6
|
+
month: "2-digit",
|
|
7
|
+
day: "2-digit"
|
|
8
|
+
}).format(/* @__PURE__ */ new Date());
|
|
9
|
+
}
|
|
10
|
+
function effectiveVersion(versions, today = todayISO()) {
|
|
11
|
+
const inForce = versions.filter((v) => v.effectiveDate <= today).sort((a, b) => b.effectiveDate.localeCompare(a.effectiveDate));
|
|
12
|
+
return inForce[0] ?? versions[versions.length - 1];
|
|
13
|
+
}
|
|
14
|
+
function upcomingVersion(versions, today = todayISO()) {
|
|
15
|
+
const announced = versions.filter((v) => v.publishedDate <= today && v.effectiveDate > today).sort((a, b) => a.effectiveDate.localeCompare(b.effectiveDate));
|
|
16
|
+
return announced[0] ?? null;
|
|
17
|
+
}
|
|
18
|
+
function policyInForce(effectiveDate, now = todayISO()) {
|
|
19
|
+
return now >= effectiveDate;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { effectiveVersion, policyInForce, todayISO, upcomingVersion };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The kind of legal document. Apps typically ship terms + privacy; `identity`
|
|
5
|
+
* is the app-agnostic SSO/identity statement (L1) used as the single privacy URL
|
|
6
|
+
* on a Google OAuth consent screen — it never enumerates downstream apps.
|
|
7
|
+
*/
|
|
8
|
+
type LegalDocKind = "terms" | "privacy" | "identity";
|
|
9
|
+
/**
|
|
10
|
+
* Whether a document is exposed on the public hub. Anything not explicitly
|
|
11
|
+
* `public` stays off the hub; create flows default to `internal`.
|
|
12
|
+
*/
|
|
13
|
+
type LegalVisibility = "public" | "internal";
|
|
14
|
+
/**
|
|
15
|
+
* One row of a version-to-version diff table (변경 전 / 변경 후).
|
|
16
|
+
* `before`/`after` support `==highlight==` markup — the renderer wraps the
|
|
17
|
+
* delimited span in a <mark>.
|
|
18
|
+
*/
|
|
19
|
+
interface VersionChange {
|
|
20
|
+
/** Section label this change belongs to (e.g. "제4조 (보존)"). */
|
|
21
|
+
section: string;
|
|
22
|
+
type: "added" | "removed" | "changed";
|
|
23
|
+
/** Prior text. Omit for `added`. May contain `==highlight==` spans. */
|
|
24
|
+
before?: string;
|
|
25
|
+
/** New text. Omit for `removed`. May contain `==highlight==` spans. */
|
|
26
|
+
after?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A titled section of a legal document.
|
|
30
|
+
*
|
|
31
|
+
* `id` is a stable deep-link slug — the public hub renders the anchor
|
|
32
|
+
* `#${docAnchor}-${id}`; assign it once and never rename or reuse it (app
|
|
33
|
+
* footers link to it). `body` is serializable Markdown, which the server-side
|
|
34
|
+
* hub renders (it has no app bundle). `content` (app JSX) is a deprecated
|
|
35
|
+
* fallback the React `LegalDocument` uses only when `body` is empty.
|
|
36
|
+
*/
|
|
37
|
+
interface LegalSection {
|
|
38
|
+
/** Stable slug for deep-linking. Hub anchor is `${docAnchor}-${id}`. */
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
/** Serializable Markdown — rendered by the hub and as the LegalDocument default. */
|
|
42
|
+
body: string;
|
|
43
|
+
/** @deprecated In-app JSX. Ignored by the hub; LegalDocument falls back to it when `body` is empty. */
|
|
44
|
+
content?: ReactNode;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A single dated version of a legal document. Newest first in the array.
|
|
48
|
+
*
|
|
49
|
+
* Publish a change by appending a new entry with a future `effectiveDate`.
|
|
50
|
+
* While `publishedDate <= today < effectiveDate` the version is "announced"
|
|
51
|
+
* (공시) — `upcomingVersion()` returns it and the notice/banner show it. On
|
|
52
|
+
* `effectiveDate` it silently becomes the one `effectiveVersion()` returns; no
|
|
53
|
+
* second commit or scheduled job is needed (both are pure date functions).
|
|
54
|
+
*
|
|
55
|
+
* Korean law: 이용약관 개정은 시행 7일 전(이용자에게 불리하면 30일 전) 공지,
|
|
56
|
+
* 개인정보처리방침은 변경 내용 공개 + 이전 버전 열람. `adverse` marks the 30-day case.
|
|
57
|
+
*/
|
|
58
|
+
interface LegalVersion {
|
|
59
|
+
/** e.g. "1.0". Must be unique within a document. */
|
|
60
|
+
version: string;
|
|
61
|
+
/** 공고일 (YYYY-MM-DD). A version only counts as announced once this is reached. */
|
|
62
|
+
publishedDate: string;
|
|
63
|
+
/** 시행일 (YYYY-MM-DD). The version takes force on this date. */
|
|
64
|
+
effectiveDate: string;
|
|
65
|
+
/** 변경 요약 — shown in the advance-notice card. */
|
|
66
|
+
summary?: string;
|
|
67
|
+
/** 이용자에게 불리/중대한 변경 → 30일 사전공지(아니면 7일). */
|
|
68
|
+
adverse?: boolean;
|
|
69
|
+
sections: LegalSection[];
|
|
70
|
+
/** Diff vs the immediately-previous version, for the 3-column table. */
|
|
71
|
+
changes?: VersionChange[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The data controller (개인정보처리자) surfaced to users, plus the privacy
|
|
75
|
+
* officer (개인정보 보호책임자) contact. For an L2 doc this is the service; for
|
|
76
|
+
* the L1 identity doc it is the identity broker (authentik).
|
|
77
|
+
*/
|
|
78
|
+
interface DataController {
|
|
79
|
+
/** 개인정보처리자 명 (e.g. service or operator name). */
|
|
80
|
+
name: string;
|
|
81
|
+
/** 개인정보 보호책임자 연락처 (reachable email). */
|
|
82
|
+
contactEmail: string;
|
|
83
|
+
}
|
|
84
|
+
/** A complete legal document: its identity plus every version (newest first). */
|
|
85
|
+
interface LegalDocMeta {
|
|
86
|
+
kind: LegalDocKind;
|
|
87
|
+
/** Display title, e.g. "이용약관". */
|
|
88
|
+
title: string;
|
|
89
|
+
/** Versions, newest first. */
|
|
90
|
+
versions: LegalVersion[];
|
|
91
|
+
/**
|
|
92
|
+
* Standalone slug owned by the legal store (key `legal:doc:${serviceId}:${kind}`).
|
|
93
|
+
* NOT a hostname — it does not join the host-keyed maintenance registry.
|
|
94
|
+
* Use "_identity" for the L1 document.
|
|
95
|
+
*/
|
|
96
|
+
serviceId: string;
|
|
97
|
+
/** Stable per-document hub anchor. Section anchors are `${docAnchor}-${section.id}`. */
|
|
98
|
+
docAnchor: string;
|
|
99
|
+
/** Gates hub rendering and PolicyNotice. Defaults to "internal" when authored. */
|
|
100
|
+
visibility: LegalVisibility;
|
|
101
|
+
/** Controller surfaced to users (L2 = the service; L1 = the identity broker). */
|
|
102
|
+
controller: DataController;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Today as YYYY-MM-DD in a given IANA timezone (default Asia/Seoul).
|
|
107
|
+
*
|
|
108
|
+
* Effective/published dates are legal calendar dates, so they must be compared
|
|
109
|
+
* against the calendar day in the service's jurisdiction — not UTC, which can
|
|
110
|
+
* be a day off for KST users near midnight.
|
|
111
|
+
*/
|
|
112
|
+
declare function todayISO(timeZone?: string): string;
|
|
113
|
+
/**
|
|
114
|
+
* The version currently in force: newest whose `effectiveDate` is on/before
|
|
115
|
+
* `today`. Falls back to the oldest version if none is effective yet (so a
|
|
116
|
+
* brand-new doc still renders something).
|
|
117
|
+
*/
|
|
118
|
+
declare function effectiveVersion(versions: LegalVersion[], today?: string): LegalVersion;
|
|
119
|
+
/**
|
|
120
|
+
* The soonest *announced* version not yet in force: `publishedDate <= today <
|
|
121
|
+
* effectiveDate`. Returns null when nothing is pending.
|
|
122
|
+
*
|
|
123
|
+
* The `publishedDate <= today` guard is deliberate — it prevents a draft entry
|
|
124
|
+
* with a future publish date from prematurely lighting up the public "변경 예정"
|
|
125
|
+
* banner (see planning concepts/user-facing-content-hygiene: no fake notices).
|
|
126
|
+
*/
|
|
127
|
+
declare function upcomingVersion(versions: LegalVersion[], today?: string): LegalVersion | null;
|
|
128
|
+
/**
|
|
129
|
+
* Date-gate check for `legal-deploy-gate` consumers (see planning concept page
|
|
130
|
+
* of the same name): "is the policy that takes effect on `effectiveDate`
|
|
131
|
+
* currently in force?"
|
|
132
|
+
*
|
|
133
|
+
* Returns `true` once `now` (KST calendar day) is on or after `effectiveDate`,
|
|
134
|
+
* so the new-policy code path activates automatically on 시행일 and stays on
|
|
135
|
+
* forever after. Pure date comparison — no hub fetch, no I/O.
|
|
136
|
+
*
|
|
137
|
+
* Typical use:
|
|
138
|
+
*
|
|
139
|
+
* if (policyInForce("2026-07-01")) {
|
|
140
|
+
* // new-policy behavior
|
|
141
|
+
* } else {
|
|
142
|
+
* // old-policy behavior (still required during the notice window)
|
|
143
|
+
* }
|
|
144
|
+
*
|
|
145
|
+
* `effectiveDate` MUST be the YYYY-MM-DD string from the legal version it
|
|
146
|
+
* matches. Hardcoding is preferred — published versions are immutable so the
|
|
147
|
+
* date won't move out from under the gate, and grepping for the literal is
|
|
148
|
+
* how the branch gets cleaned up after the migration window closes.
|
|
149
|
+
*/
|
|
150
|
+
declare function policyInForce(effectiveDate: string, now?: string): boolean;
|
|
151
|
+
|
|
152
|
+
export { type DataController as D, type LegalDocMeta as L, type VersionChange as V, type LegalVersion as a, type LegalDocKind as b, type LegalSection as c, type LegalVisibility as d, effectiveVersion as e, policyInForce as p, todayISO as t, upcomingVersion as u };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The kind of legal document. Apps typically ship terms + privacy; `identity`
|
|
5
|
+
* is the app-agnostic SSO/identity statement (L1) used as the single privacy URL
|
|
6
|
+
* on a Google OAuth consent screen — it never enumerates downstream apps.
|
|
7
|
+
*/
|
|
8
|
+
type LegalDocKind = "terms" | "privacy" | "identity";
|
|
9
|
+
/**
|
|
10
|
+
* Whether a document is exposed on the public hub. Anything not explicitly
|
|
11
|
+
* `public` stays off the hub; create flows default to `internal`.
|
|
12
|
+
*/
|
|
13
|
+
type LegalVisibility = "public" | "internal";
|
|
14
|
+
/**
|
|
15
|
+
* One row of a version-to-version diff table (변경 전 / 변경 후).
|
|
16
|
+
* `before`/`after` support `==highlight==` markup — the renderer wraps the
|
|
17
|
+
* delimited span in a <mark>.
|
|
18
|
+
*/
|
|
19
|
+
interface VersionChange {
|
|
20
|
+
/** Section label this change belongs to (e.g. "제4조 (보존)"). */
|
|
21
|
+
section: string;
|
|
22
|
+
type: "added" | "removed" | "changed";
|
|
23
|
+
/** Prior text. Omit for `added`. May contain `==highlight==` spans. */
|
|
24
|
+
before?: string;
|
|
25
|
+
/** New text. Omit for `removed`. May contain `==highlight==` spans. */
|
|
26
|
+
after?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A titled section of a legal document.
|
|
30
|
+
*
|
|
31
|
+
* `id` is a stable deep-link slug — the public hub renders the anchor
|
|
32
|
+
* `#${docAnchor}-${id}`; assign it once and never rename or reuse it (app
|
|
33
|
+
* footers link to it). `body` is serializable Markdown, which the server-side
|
|
34
|
+
* hub renders (it has no app bundle). `content` (app JSX) is a deprecated
|
|
35
|
+
* fallback the React `LegalDocument` uses only when `body` is empty.
|
|
36
|
+
*/
|
|
37
|
+
interface LegalSection {
|
|
38
|
+
/** Stable slug for deep-linking. Hub anchor is `${docAnchor}-${id}`. */
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
/** Serializable Markdown — rendered by the hub and as the LegalDocument default. */
|
|
42
|
+
body: string;
|
|
43
|
+
/** @deprecated In-app JSX. Ignored by the hub; LegalDocument falls back to it when `body` is empty. */
|
|
44
|
+
content?: ReactNode;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A single dated version of a legal document. Newest first in the array.
|
|
48
|
+
*
|
|
49
|
+
* Publish a change by appending a new entry with a future `effectiveDate`.
|
|
50
|
+
* While `publishedDate <= today < effectiveDate` the version is "announced"
|
|
51
|
+
* (공시) — `upcomingVersion()` returns it and the notice/banner show it. On
|
|
52
|
+
* `effectiveDate` it silently becomes the one `effectiveVersion()` returns; no
|
|
53
|
+
* second commit or scheduled job is needed (both are pure date functions).
|
|
54
|
+
*
|
|
55
|
+
* Korean law: 이용약관 개정은 시행 7일 전(이용자에게 불리하면 30일 전) 공지,
|
|
56
|
+
* 개인정보처리방침은 변경 내용 공개 + 이전 버전 열람. `adverse` marks the 30-day case.
|
|
57
|
+
*/
|
|
58
|
+
interface LegalVersion {
|
|
59
|
+
/** e.g. "1.0". Must be unique within a document. */
|
|
60
|
+
version: string;
|
|
61
|
+
/** 공고일 (YYYY-MM-DD). A version only counts as announced once this is reached. */
|
|
62
|
+
publishedDate: string;
|
|
63
|
+
/** 시행일 (YYYY-MM-DD). The version takes force on this date. */
|
|
64
|
+
effectiveDate: string;
|
|
65
|
+
/** 변경 요약 — shown in the advance-notice card. */
|
|
66
|
+
summary?: string;
|
|
67
|
+
/** 이용자에게 불리/중대한 변경 → 30일 사전공지(아니면 7일). */
|
|
68
|
+
adverse?: boolean;
|
|
69
|
+
sections: LegalSection[];
|
|
70
|
+
/** Diff vs the immediately-previous version, for the 3-column table. */
|
|
71
|
+
changes?: VersionChange[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The data controller (개인정보처리자) surfaced to users, plus the privacy
|
|
75
|
+
* officer (개인정보 보호책임자) contact. For an L2 doc this is the service; for
|
|
76
|
+
* the L1 identity doc it is the identity broker (authentik).
|
|
77
|
+
*/
|
|
78
|
+
interface DataController {
|
|
79
|
+
/** 개인정보처리자 명 (e.g. service or operator name). */
|
|
80
|
+
name: string;
|
|
81
|
+
/** 개인정보 보호책임자 연락처 (reachable email). */
|
|
82
|
+
contactEmail: string;
|
|
83
|
+
}
|
|
84
|
+
/** A complete legal document: its identity plus every version (newest first). */
|
|
85
|
+
interface LegalDocMeta {
|
|
86
|
+
kind: LegalDocKind;
|
|
87
|
+
/** Display title, e.g. "이용약관". */
|
|
88
|
+
title: string;
|
|
89
|
+
/** Versions, newest first. */
|
|
90
|
+
versions: LegalVersion[];
|
|
91
|
+
/**
|
|
92
|
+
* Standalone slug owned by the legal store (key `legal:doc:${serviceId}:${kind}`).
|
|
93
|
+
* NOT a hostname — it does not join the host-keyed maintenance registry.
|
|
94
|
+
* Use "_identity" for the L1 document.
|
|
95
|
+
*/
|
|
96
|
+
serviceId: string;
|
|
97
|
+
/** Stable per-document hub anchor. Section anchors are `${docAnchor}-${section.id}`. */
|
|
98
|
+
docAnchor: string;
|
|
99
|
+
/** Gates hub rendering and PolicyNotice. Defaults to "internal" when authored. */
|
|
100
|
+
visibility: LegalVisibility;
|
|
101
|
+
/** Controller surfaced to users (L2 = the service; L1 = the identity broker). */
|
|
102
|
+
controller: DataController;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Today as YYYY-MM-DD in a given IANA timezone (default Asia/Seoul).
|
|
107
|
+
*
|
|
108
|
+
* Effective/published dates are legal calendar dates, so they must be compared
|
|
109
|
+
* against the calendar day in the service's jurisdiction — not UTC, which can
|
|
110
|
+
* be a day off for KST users near midnight.
|
|
111
|
+
*/
|
|
112
|
+
declare function todayISO(timeZone?: string): string;
|
|
113
|
+
/**
|
|
114
|
+
* The version currently in force: newest whose `effectiveDate` is on/before
|
|
115
|
+
* `today`. Falls back to the oldest version if none is effective yet (so a
|
|
116
|
+
* brand-new doc still renders something).
|
|
117
|
+
*/
|
|
118
|
+
declare function effectiveVersion(versions: LegalVersion[], today?: string): LegalVersion;
|
|
119
|
+
/**
|
|
120
|
+
* The soonest *announced* version not yet in force: `publishedDate <= today <
|
|
121
|
+
* effectiveDate`. Returns null when nothing is pending.
|
|
122
|
+
*
|
|
123
|
+
* The `publishedDate <= today` guard is deliberate — it prevents a draft entry
|
|
124
|
+
* with a future publish date from prematurely lighting up the public "변경 예정"
|
|
125
|
+
* banner (see planning concepts/user-facing-content-hygiene: no fake notices).
|
|
126
|
+
*/
|
|
127
|
+
declare function upcomingVersion(versions: LegalVersion[], today?: string): LegalVersion | null;
|
|
128
|
+
/**
|
|
129
|
+
* Date-gate check for `legal-deploy-gate` consumers (see planning concept page
|
|
130
|
+
* of the same name): "is the policy that takes effect on `effectiveDate`
|
|
131
|
+
* currently in force?"
|
|
132
|
+
*
|
|
133
|
+
* Returns `true` once `now` (KST calendar day) is on or after `effectiveDate`,
|
|
134
|
+
* so the new-policy code path activates automatically on 시행일 and stays on
|
|
135
|
+
* forever after. Pure date comparison — no hub fetch, no I/O.
|
|
136
|
+
*
|
|
137
|
+
* Typical use:
|
|
138
|
+
*
|
|
139
|
+
* if (policyInForce("2026-07-01")) {
|
|
140
|
+
* // new-policy behavior
|
|
141
|
+
* } else {
|
|
142
|
+
* // old-policy behavior (still required during the notice window)
|
|
143
|
+
* }
|
|
144
|
+
*
|
|
145
|
+
* `effectiveDate` MUST be the YYYY-MM-DD string from the legal version it
|
|
146
|
+
* matches. Hardcoding is preferred — published versions are immutable so the
|
|
147
|
+
* date won't move out from under the gate, and grepping for the literal is
|
|
148
|
+
* how the branch gets cleaned up after the migration window closes.
|
|
149
|
+
*/
|
|
150
|
+
declare function policyInForce(effectiveDate: string, now?: string): boolean;
|
|
151
|
+
|
|
152
|
+
export { type DataController as D, type LegalDocMeta as L, type VersionChange as V, type LegalVersion as a, type LegalDocKind as b, type LegalSection as c, type LegalVisibility as d, effectiveVersion as e, policyInForce as p, todayISO as t, upcomingVersion as u };
|
package/dist/helpers.cjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/helpers.ts
|
|
4
|
+
function todayISO(timeZone = "Asia/Seoul") {
|
|
5
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
6
|
+
timeZone,
|
|
7
|
+
year: "numeric",
|
|
8
|
+
month: "2-digit",
|
|
9
|
+
day: "2-digit"
|
|
10
|
+
}).format(/* @__PURE__ */ new Date());
|
|
11
|
+
}
|
|
12
|
+
function effectiveVersion(versions, today = todayISO()) {
|
|
13
|
+
const inForce = versions.filter((v) => v.effectiveDate <= today).sort((a, b) => b.effectiveDate.localeCompare(a.effectiveDate));
|
|
14
|
+
return inForce[0] ?? versions[versions.length - 1];
|
|
15
|
+
}
|
|
16
|
+
function upcomingVersion(versions, today = todayISO()) {
|
|
17
|
+
const announced = versions.filter((v) => v.publishedDate <= today && v.effectiveDate > today).sort((a, b) => a.effectiveDate.localeCompare(b.effectiveDate));
|
|
18
|
+
return announced[0] ?? null;
|
|
19
|
+
}
|
|
20
|
+
function policyInForce(effectiveDate, now = todayISO()) {
|
|
21
|
+
return now >= effectiveDate;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
exports.effectiveVersion = effectiveVersion;
|
|
25
|
+
exports.policyInForce = policyInForce;
|
|
26
|
+
exports.todayISO = todayISO;
|
|
27
|
+
exports.upcomingVersion = upcomingVersion;
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { effectiveVersion, policyInForce, todayISO, upcomingVersion } from './chunk-MDFCRQ5H.js';
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
var react = require('react');
|
|
5
|
+
|
|
6
|
+
// src/helpers.ts
|
|
7
|
+
function todayISO(timeZone = "Asia/Seoul") {
|
|
8
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
9
|
+
timeZone,
|
|
10
|
+
year: "numeric",
|
|
11
|
+
month: "2-digit",
|
|
12
|
+
day: "2-digit"
|
|
13
|
+
}).format(/* @__PURE__ */ new Date());
|
|
14
|
+
}
|
|
15
|
+
function effectiveVersion(versions, today = todayISO()) {
|
|
16
|
+
const inForce = versions.filter((v) => v.effectiveDate <= today).sort((a, b) => b.effectiveDate.localeCompare(a.effectiveDate));
|
|
17
|
+
return inForce[0] ?? versions[versions.length - 1];
|
|
18
|
+
}
|
|
19
|
+
function upcomingVersion(versions, today = todayISO()) {
|
|
20
|
+
const announced = versions.filter((v) => v.publishedDate <= today && v.effectiveDate > today).sort((a, b) => a.effectiveDate.localeCompare(b.effectiveDate));
|
|
21
|
+
return announced[0] ?? null;
|
|
22
|
+
}
|
|
23
|
+
function policyInForce(effectiveDate, now = todayISO()) {
|
|
24
|
+
return now >= effectiveDate;
|
|
25
|
+
}
|
|
26
|
+
function renderHighlighted(text) {
|
|
27
|
+
return text.split(/(==.*?==)/g).map((part, i) => {
|
|
28
|
+
if (part.startsWith("==") && part.endsWith("==") && part.length >= 4) {
|
|
29
|
+
return /* @__PURE__ */ jsxRuntime.jsx("mark", { className: "legal-mark", children: part.slice(2, -2) }, i);
|
|
30
|
+
}
|
|
31
|
+
if (part.includes("\n")) {
|
|
32
|
+
const lines = part.split("\n");
|
|
33
|
+
return lines.map((line, j) => /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
34
|
+
line,
|
|
35
|
+
j < lines.length - 1 && /* @__PURE__ */ jsxRuntime.jsx("br", {})
|
|
36
|
+
] }, `${i}-${j}`));
|
|
37
|
+
}
|
|
38
|
+
return part;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function LegalDocument({
|
|
42
|
+
doc,
|
|
43
|
+
version,
|
|
44
|
+
className
|
|
45
|
+
}) {
|
|
46
|
+
const v = version ?? effectiveVersion(doc.versions);
|
|
47
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
48
|
+
"article",
|
|
49
|
+
{
|
|
50
|
+
id: doc.docAnchor,
|
|
51
|
+
className: className ? `legal-document ${className}` : "legal-document",
|
|
52
|
+
children: v.sections.map((section) => /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "legal-section", children: [
|
|
53
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { id: `${doc.docAnchor}-${section.id}`, className: "legal-section-title", children: section.title }),
|
|
54
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "legal-section-body", children: section.content ?? section.body })
|
|
55
|
+
] }, section.id))
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
var badgeLabel = {
|
|
60
|
+
added: "\uCD94\uAC00",
|
|
61
|
+
removed: "\uC0AD\uC81C",
|
|
62
|
+
changed: "\uBCC0\uACBD"
|
|
63
|
+
};
|
|
64
|
+
function VersionDiff({
|
|
65
|
+
changes,
|
|
66
|
+
fromVersion,
|
|
67
|
+
toVersion,
|
|
68
|
+
className
|
|
69
|
+
}) {
|
|
70
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: className ? `legal-diff ${className}` : "legal-diff", children: [
|
|
71
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "legal-diff-title", children: [
|
|
72
|
+
"\uC8FC\uC694 \uBCC0\uACBD \uC0AC\uD56D (v",
|
|
73
|
+
fromVersion,
|
|
74
|
+
" \u2192 v",
|
|
75
|
+
toVersion,
|
|
76
|
+
")"
|
|
77
|
+
] }),
|
|
78
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "legal-diff-scroll", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "legal-diff-table", children: [
|
|
79
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
80
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { children: "\uAD6C\uBD84" }),
|
|
81
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { children: "\uBCC0\uACBD \uC804" }),
|
|
82
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { children: "\uBCC0\uACBD \uD6C4" })
|
|
83
|
+
] }) }),
|
|
84
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: changes.map((c, i) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
85
|
+
/* @__PURE__ */ jsxRuntime.jsxs("td", { className: "legal-diff-section", children: [
|
|
86
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: c.section }),
|
|
87
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `legal-badge legal-badge--${c.type}`, children: badgeLabel[c.type] })
|
|
88
|
+
] }),
|
|
89
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { children: c.type === "added" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "legal-diff-empty", children: "\u2014" }) : renderHighlighted(c.before ?? "") }),
|
|
90
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { children: c.type === "removed" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "legal-diff-empty", children: "\u2014" }) : renderHighlighted(c.after ?? "") })
|
|
91
|
+
] }, i)) })
|
|
92
|
+
] }) })
|
|
93
|
+
] });
|
|
94
|
+
}
|
|
95
|
+
function VersionHistory({
|
|
96
|
+
versions,
|
|
97
|
+
selectedIdx,
|
|
98
|
+
onSelect,
|
|
99
|
+
className
|
|
100
|
+
}) {
|
|
101
|
+
if (versions.length <= 1) return null;
|
|
102
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: className ? `legal-history ${className}` : "legal-history", children: [
|
|
103
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "legal-history-title", children: "\uC774\uC804 \uBC84\uC804" }),
|
|
104
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "legal-history-list", children: versions.map((v, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
105
|
+
"button",
|
|
106
|
+
{
|
|
107
|
+
type: "button",
|
|
108
|
+
onClick: () => onSelect(i),
|
|
109
|
+
"aria-current": i === selectedIdx ? "true" : void 0,
|
|
110
|
+
className: i === selectedIdx ? "legal-history-item is-selected" : "legal-history-item",
|
|
111
|
+
children: [
|
|
112
|
+
"v",
|
|
113
|
+
v.version,
|
|
114
|
+
" (",
|
|
115
|
+
v.effectiveDate,
|
|
116
|
+
")"
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
) }, v.version)) })
|
|
120
|
+
] });
|
|
121
|
+
}
|
|
122
|
+
function UpcomingNotice({
|
|
123
|
+
docTitle,
|
|
124
|
+
version,
|
|
125
|
+
onPreview,
|
|
126
|
+
className
|
|
127
|
+
}) {
|
|
128
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: className ? `legal-notice ${className}` : "legal-notice", children: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "legal-notice-text", children: [
|
|
129
|
+
"\u{1F4E2} ",
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: docTitle }),
|
|
131
|
+
" \uBCC0\uACBD \uC608\uC815 \u2014 ",
|
|
132
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: version.effectiveDate }),
|
|
133
|
+
" \uC2DC\uD589 (v",
|
|
134
|
+
version.version,
|
|
135
|
+
").",
|
|
136
|
+
version.summary ? ` ${version.summary}` : "",
|
|
137
|
+
version.adverse ? " \uC774\uC6A9\uC790\uC5D0\uAC8C \uBD88\uB9AC/\uC911\uB300\uD55C \uBCC0\uACBD\uC73C\uB85C \uC2DC\uD589 30\uC77C \uC804\uBD80\uD130 \uACF5\uC9C0\uD569\uB2C8\uB2E4." : ` (\uACF5\uACE0\uC77C ${version.publishedDate})`,
|
|
138
|
+
onPreview && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
139
|
+
" ",
|
|
140
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "legal-notice-preview", onClick: onPreview, children: "\uBCC0\uACBD \uC608\uC815\uBCF8 \uBCF4\uAE30" })
|
|
141
|
+
] })
|
|
142
|
+
] }) });
|
|
143
|
+
}
|
|
144
|
+
var defaultRenderLink = (href, children) => /* @__PURE__ */ jsxRuntime.jsx("a", { href, children });
|
|
145
|
+
function PolicyNotice({
|
|
146
|
+
docs,
|
|
147
|
+
hrefFor,
|
|
148
|
+
today,
|
|
149
|
+
renderLink = defaultRenderLink,
|
|
150
|
+
storage,
|
|
151
|
+
className
|
|
152
|
+
}) {
|
|
153
|
+
const store = storage ?? (typeof localStorage !== "undefined" ? localStorage : void 0);
|
|
154
|
+
const pending = docs.filter((doc) => doc.visibility === "public").map((doc) => ({ doc, up: upcomingVersion(doc.versions, today) })).filter((x) => x.up != null);
|
|
155
|
+
const dismissKey = "legal-policy-notice:" + pending.map((x) => `${x.doc.serviceId}:${x.doc.kind}@${x.up.version}`).join(",");
|
|
156
|
+
const [dismissed, setDismissed] = react.useState(() => store?.getItem(dismissKey) === "1");
|
|
157
|
+
if (pending.length === 0 || dismissed) return null;
|
|
158
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: className ? `legal-policy-notice ${className}` : "legal-policy-notice", children: [
|
|
159
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "legal-policy-notice-text", children: [
|
|
160
|
+
"\u{1F4E2}",
|
|
161
|
+
" ",
|
|
162
|
+
pending.map((x, i) => /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
163
|
+
i > 0 ? " \xB7 " : "",
|
|
164
|
+
renderLink(hrefFor(x.doc), x.doc.title),
|
|
165
|
+
" ",
|
|
166
|
+
x.up.effectiveDate,
|
|
167
|
+
"\uBD80\uD130 \uBCC0\uACBD \uC608\uC815"
|
|
168
|
+
] }, `${x.doc.serviceId}:${x.doc.kind}`))
|
|
169
|
+
] }),
|
|
170
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
171
|
+
"button",
|
|
172
|
+
{
|
|
173
|
+
type: "button",
|
|
174
|
+
className: "legal-policy-notice-dismiss",
|
|
175
|
+
"aria-label": "\uB2EB\uAE30",
|
|
176
|
+
onClick: () => {
|
|
177
|
+
store?.setItem(dismissKey, "1");
|
|
178
|
+
setDismissed(true);
|
|
179
|
+
},
|
|
180
|
+
children: "\u2715"
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
] });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
exports.LegalDocument = LegalDocument;
|
|
187
|
+
exports.PolicyNotice = PolicyNotice;
|
|
188
|
+
exports.UpcomingNotice = UpcomingNotice;
|
|
189
|
+
exports.VersionDiff = VersionDiff;
|
|
190
|
+
exports.VersionHistory = VersionHistory;
|
|
191
|
+
exports.effectiveVersion = effectiveVersion;
|
|
192
|
+
exports.policyInForce = policyInForce;
|
|
193
|
+
exports.renderHighlighted = renderHighlighted;
|
|
194
|
+
exports.todayISO = todayISO;
|
|
195
|
+
exports.upcomingVersion = upcomingVersion;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { L as LegalDocMeta, a as LegalVersion, V as VersionChange } from './helpers-CRGM1DHb.cjs';
|
|
2
|
+
export { D as DataController, b as LegalDocKind, c as LegalSection, d as LegalVisibility, e as effectiveVersion, p as policyInForce, t as todayISO, u as upcomingVersion } from './helpers-CRGM1DHb.cjs';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Render text with `==highlight==` spans wrapped in <mark class="legal-mark">,
|
|
8
|
+
* and bare "\n" turned into <br/>. Used by the diff table to call out the exact
|
|
9
|
+
* words that changed between two versions.
|
|
10
|
+
*/
|
|
11
|
+
declare function renderHighlighted(text: string): ReactNode;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Renders the titled sections of a legal document's effective version, with
|
|
15
|
+
* stable deep-link anchors: `${doc.docAnchor}` on the article and
|
|
16
|
+
* `${doc.docAnchor}-${section.id}` on each heading. Pass `version` to render a
|
|
17
|
+
* specific version instead of the effective one.
|
|
18
|
+
*
|
|
19
|
+
* Each section renders its Markdown `body` as text; the deprecated `content`
|
|
20
|
+
* (app JSX) is used only as a fallback when `body` is empty. Markdown is not
|
|
21
|
+
* parsed here — the public hub renders Markdown server-side; this component is
|
|
22
|
+
* for in-app JSX docs.
|
|
23
|
+
*/
|
|
24
|
+
declare function LegalDocument({ doc, version, className, }: {
|
|
25
|
+
doc: LegalDocMeta;
|
|
26
|
+
version?: LegalVersion;
|
|
27
|
+
className?: string;
|
|
28
|
+
}): react.JSX.Element;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 3-column "변경 전 / 변경 후" table for the diff between two versions.
|
|
32
|
+
* `==highlight==` spans in before/after text are marked. Styleless — themed via
|
|
33
|
+
* the `legal-*` classes / CSS variables in the shipped stylesheet.
|
|
34
|
+
*/
|
|
35
|
+
declare function VersionDiff({ changes, fromVersion, toVersion, className, }: {
|
|
36
|
+
changes: VersionChange[];
|
|
37
|
+
fromVersion: string;
|
|
38
|
+
toVersion: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
}): react.JSX.Element;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Version picker / 이전 버전 list. Controlled: the parent owns `selectedIdx`
|
|
44
|
+
* and updates it from `onSelect`. Renders nothing for a single-version doc.
|
|
45
|
+
*/
|
|
46
|
+
declare function VersionHistory({ versions, selectedIdx, onSelect, className, }: {
|
|
47
|
+
versions: LegalVersion[];
|
|
48
|
+
selectedIdx: number;
|
|
49
|
+
onSelect: (idx: number) => void;
|
|
50
|
+
className?: string;
|
|
51
|
+
}): react.JSX.Element | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Per-document advance-notice card (공시): "v2.0이 YYYY-MM-DD부터 시행됩니다".
|
|
55
|
+
* Render it on a document page when `upcomingVersion(doc.versions)` is non-null.
|
|
56
|
+
* `onPreview` (optional) lets the page switch the shown version to the upcoming one.
|
|
57
|
+
*/
|
|
58
|
+
declare function UpcomingNotice({ docTitle, version, onPreview, className, }: {
|
|
59
|
+
docTitle: string;
|
|
60
|
+
version: LegalVersion;
|
|
61
|
+
onPreview?: () => void;
|
|
62
|
+
className?: string;
|
|
63
|
+
}): react.JSX.Element;
|
|
64
|
+
|
|
65
|
+
/** Router-agnostic link renderer. Defaults to a plain anchor. */
|
|
66
|
+
type RenderLink = (href: string, children: ReactNode) => ReactNode;
|
|
67
|
+
/**
|
|
68
|
+
* Site-wide advance-notice banner (사이트 공통 사전공시 배너): shows when any
|
|
69
|
+
* document has an announced-but-not-yet-effective version. Dismissible per
|
|
70
|
+
* pending-version-set (so a new announcement re-shows it). Renders nothing when
|
|
71
|
+
* nothing is pending.
|
|
72
|
+
*
|
|
73
|
+
* Router-agnostic: pass `renderLink` to use the app's Link (next/link,
|
|
74
|
+
* react-router) instead of a plain <a>.
|
|
75
|
+
*
|
|
76
|
+
* Interactive (uses state) — mount inside a client boundary (Next.js "use client").
|
|
77
|
+
*/
|
|
78
|
+
declare function PolicyNotice({ docs, hrefFor, today, renderLink, storage, className, }: {
|
|
79
|
+
docs: LegalDocMeta[];
|
|
80
|
+
/** Maps a document to its route, e.g. (doc) => `/${doc.kind}`. */
|
|
81
|
+
hrefFor: (doc: LegalDocMeta) => string;
|
|
82
|
+
today?: string;
|
|
83
|
+
renderLink?: RenderLink;
|
|
84
|
+
/** Persistence for the dismissed flag. Defaults to localStorage when available. */
|
|
85
|
+
storage?: Pick<Storage, "getItem" | "setItem">;
|
|
86
|
+
className?: string;
|
|
87
|
+
}): react.JSX.Element | null;
|
|
88
|
+
|
|
89
|
+
export { LegalDocMeta, LegalDocument, LegalVersion, PolicyNotice, type RenderLink, UpcomingNotice, VersionChange, VersionDiff, VersionHistory, renderHighlighted };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { L as LegalDocMeta, a as LegalVersion, V as VersionChange } from './helpers-CRGM1DHb.js';
|
|
2
|
+
export { D as DataController, b as LegalDocKind, c as LegalSection, d as LegalVisibility, e as effectiveVersion, p as policyInForce, t as todayISO, u as upcomingVersion } from './helpers-CRGM1DHb.js';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Render text with `==highlight==` spans wrapped in <mark class="legal-mark">,
|
|
8
|
+
* and bare "\n" turned into <br/>. Used by the diff table to call out the exact
|
|
9
|
+
* words that changed between two versions.
|
|
10
|
+
*/
|
|
11
|
+
declare function renderHighlighted(text: string): ReactNode;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Renders the titled sections of a legal document's effective version, with
|
|
15
|
+
* stable deep-link anchors: `${doc.docAnchor}` on the article and
|
|
16
|
+
* `${doc.docAnchor}-${section.id}` on each heading. Pass `version` to render a
|
|
17
|
+
* specific version instead of the effective one.
|
|
18
|
+
*
|
|
19
|
+
* Each section renders its Markdown `body` as text; the deprecated `content`
|
|
20
|
+
* (app JSX) is used only as a fallback when `body` is empty. Markdown is not
|
|
21
|
+
* parsed here — the public hub renders Markdown server-side; this component is
|
|
22
|
+
* for in-app JSX docs.
|
|
23
|
+
*/
|
|
24
|
+
declare function LegalDocument({ doc, version, className, }: {
|
|
25
|
+
doc: LegalDocMeta;
|
|
26
|
+
version?: LegalVersion;
|
|
27
|
+
className?: string;
|
|
28
|
+
}): react.JSX.Element;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 3-column "변경 전 / 변경 후" table for the diff between two versions.
|
|
32
|
+
* `==highlight==` spans in before/after text are marked. Styleless — themed via
|
|
33
|
+
* the `legal-*` classes / CSS variables in the shipped stylesheet.
|
|
34
|
+
*/
|
|
35
|
+
declare function VersionDiff({ changes, fromVersion, toVersion, className, }: {
|
|
36
|
+
changes: VersionChange[];
|
|
37
|
+
fromVersion: string;
|
|
38
|
+
toVersion: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
}): react.JSX.Element;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Version picker / 이전 버전 list. Controlled: the parent owns `selectedIdx`
|
|
44
|
+
* and updates it from `onSelect`. Renders nothing for a single-version doc.
|
|
45
|
+
*/
|
|
46
|
+
declare function VersionHistory({ versions, selectedIdx, onSelect, className, }: {
|
|
47
|
+
versions: LegalVersion[];
|
|
48
|
+
selectedIdx: number;
|
|
49
|
+
onSelect: (idx: number) => void;
|
|
50
|
+
className?: string;
|
|
51
|
+
}): react.JSX.Element | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Per-document advance-notice card (공시): "v2.0이 YYYY-MM-DD부터 시행됩니다".
|
|
55
|
+
* Render it on a document page when `upcomingVersion(doc.versions)` is non-null.
|
|
56
|
+
* `onPreview` (optional) lets the page switch the shown version to the upcoming one.
|
|
57
|
+
*/
|
|
58
|
+
declare function UpcomingNotice({ docTitle, version, onPreview, className, }: {
|
|
59
|
+
docTitle: string;
|
|
60
|
+
version: LegalVersion;
|
|
61
|
+
onPreview?: () => void;
|
|
62
|
+
className?: string;
|
|
63
|
+
}): react.JSX.Element;
|
|
64
|
+
|
|
65
|
+
/** Router-agnostic link renderer. Defaults to a plain anchor. */
|
|
66
|
+
type RenderLink = (href: string, children: ReactNode) => ReactNode;
|
|
67
|
+
/**
|
|
68
|
+
* Site-wide advance-notice banner (사이트 공통 사전공시 배너): shows when any
|
|
69
|
+
* document has an announced-but-not-yet-effective version. Dismissible per
|
|
70
|
+
* pending-version-set (so a new announcement re-shows it). Renders nothing when
|
|
71
|
+
* nothing is pending.
|
|
72
|
+
*
|
|
73
|
+
* Router-agnostic: pass `renderLink` to use the app's Link (next/link,
|
|
74
|
+
* react-router) instead of a plain <a>.
|
|
75
|
+
*
|
|
76
|
+
* Interactive (uses state) — mount inside a client boundary (Next.js "use client").
|
|
77
|
+
*/
|
|
78
|
+
declare function PolicyNotice({ docs, hrefFor, today, renderLink, storage, className, }: {
|
|
79
|
+
docs: LegalDocMeta[];
|
|
80
|
+
/** Maps a document to its route, e.g. (doc) => `/${doc.kind}`. */
|
|
81
|
+
hrefFor: (doc: LegalDocMeta) => string;
|
|
82
|
+
today?: string;
|
|
83
|
+
renderLink?: RenderLink;
|
|
84
|
+
/** Persistence for the dismissed flag. Defaults to localStorage when available. */
|
|
85
|
+
storage?: Pick<Storage, "getItem" | "setItem">;
|
|
86
|
+
className?: string;
|
|
87
|
+
}): react.JSX.Element | null;
|
|
88
|
+
|
|
89
|
+
export { LegalDocMeta, LegalDocument, LegalVersion, PolicyNotice, type RenderLink, UpcomingNotice, VersionChange, VersionDiff, VersionHistory, renderHighlighted };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { effectiveVersion, upcomingVersion } from './chunk-MDFCRQ5H.js';
|
|
2
|
+
export { effectiveVersion, policyInForce, todayISO, upcomingVersion } from './chunk-MDFCRQ5H.js';
|
|
3
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
function renderHighlighted(text) {
|
|
7
|
+
return text.split(/(==.*?==)/g).map((part, i) => {
|
|
8
|
+
if (part.startsWith("==") && part.endsWith("==") && part.length >= 4) {
|
|
9
|
+
return /* @__PURE__ */ jsx("mark", { className: "legal-mark", children: part.slice(2, -2) }, i);
|
|
10
|
+
}
|
|
11
|
+
if (part.includes("\n")) {
|
|
12
|
+
const lines = part.split("\n");
|
|
13
|
+
return lines.map((line, j) => /* @__PURE__ */ jsxs("span", { children: [
|
|
14
|
+
line,
|
|
15
|
+
j < lines.length - 1 && /* @__PURE__ */ jsx("br", {})
|
|
16
|
+
] }, `${i}-${j}`));
|
|
17
|
+
}
|
|
18
|
+
return part;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function LegalDocument({
|
|
22
|
+
doc,
|
|
23
|
+
version,
|
|
24
|
+
className
|
|
25
|
+
}) {
|
|
26
|
+
const v = version ?? effectiveVersion(doc.versions);
|
|
27
|
+
return /* @__PURE__ */ jsx(
|
|
28
|
+
"article",
|
|
29
|
+
{
|
|
30
|
+
id: doc.docAnchor,
|
|
31
|
+
className: className ? `legal-document ${className}` : "legal-document",
|
|
32
|
+
children: v.sections.map((section) => /* @__PURE__ */ jsxs("section", { className: "legal-section", children: [
|
|
33
|
+
/* @__PURE__ */ jsx("h2", { id: `${doc.docAnchor}-${section.id}`, className: "legal-section-title", children: section.title }),
|
|
34
|
+
/* @__PURE__ */ jsx("div", { className: "legal-section-body", children: section.content ?? section.body })
|
|
35
|
+
] }, section.id))
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
var badgeLabel = {
|
|
40
|
+
added: "\uCD94\uAC00",
|
|
41
|
+
removed: "\uC0AD\uC81C",
|
|
42
|
+
changed: "\uBCC0\uACBD"
|
|
43
|
+
};
|
|
44
|
+
function VersionDiff({
|
|
45
|
+
changes,
|
|
46
|
+
fromVersion,
|
|
47
|
+
toVersion,
|
|
48
|
+
className
|
|
49
|
+
}) {
|
|
50
|
+
return /* @__PURE__ */ jsxs("div", { className: className ? `legal-diff ${className}` : "legal-diff", children: [
|
|
51
|
+
/* @__PURE__ */ jsxs("h3", { className: "legal-diff-title", children: [
|
|
52
|
+
"\uC8FC\uC694 \uBCC0\uACBD \uC0AC\uD56D (v",
|
|
53
|
+
fromVersion,
|
|
54
|
+
" \u2192 v",
|
|
55
|
+
toVersion,
|
|
56
|
+
")"
|
|
57
|
+
] }),
|
|
58
|
+
/* @__PURE__ */ jsx("div", { className: "legal-diff-scroll", children: /* @__PURE__ */ jsxs("table", { className: "legal-diff-table", children: [
|
|
59
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
60
|
+
/* @__PURE__ */ jsx("th", { children: "\uAD6C\uBD84" }),
|
|
61
|
+
/* @__PURE__ */ jsx("th", { children: "\uBCC0\uACBD \uC804" }),
|
|
62
|
+
/* @__PURE__ */ jsx("th", { children: "\uBCC0\uACBD \uD6C4" })
|
|
63
|
+
] }) }),
|
|
64
|
+
/* @__PURE__ */ jsx("tbody", { children: changes.map((c, i) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
65
|
+
/* @__PURE__ */ jsxs("td", { className: "legal-diff-section", children: [
|
|
66
|
+
/* @__PURE__ */ jsx("span", { children: c.section }),
|
|
67
|
+
/* @__PURE__ */ jsx("span", { className: `legal-badge legal-badge--${c.type}`, children: badgeLabel[c.type] })
|
|
68
|
+
] }),
|
|
69
|
+
/* @__PURE__ */ jsx("td", { children: c.type === "added" ? /* @__PURE__ */ jsx("span", { className: "legal-diff-empty", children: "\u2014" }) : renderHighlighted(c.before ?? "") }),
|
|
70
|
+
/* @__PURE__ */ jsx("td", { children: c.type === "removed" ? /* @__PURE__ */ jsx("span", { className: "legal-diff-empty", children: "\u2014" }) : renderHighlighted(c.after ?? "") })
|
|
71
|
+
] }, i)) })
|
|
72
|
+
] }) })
|
|
73
|
+
] });
|
|
74
|
+
}
|
|
75
|
+
function VersionHistory({
|
|
76
|
+
versions,
|
|
77
|
+
selectedIdx,
|
|
78
|
+
onSelect,
|
|
79
|
+
className
|
|
80
|
+
}) {
|
|
81
|
+
if (versions.length <= 1) return null;
|
|
82
|
+
return /* @__PURE__ */ jsxs("div", { className: className ? `legal-history ${className}` : "legal-history", children: [
|
|
83
|
+
/* @__PURE__ */ jsx("h3", { className: "legal-history-title", children: "\uC774\uC804 \uBC84\uC804" }),
|
|
84
|
+
/* @__PURE__ */ jsx("ul", { className: "legal-history-list", children: versions.map((v, i) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
85
|
+
"button",
|
|
86
|
+
{
|
|
87
|
+
type: "button",
|
|
88
|
+
onClick: () => onSelect(i),
|
|
89
|
+
"aria-current": i === selectedIdx ? "true" : void 0,
|
|
90
|
+
className: i === selectedIdx ? "legal-history-item is-selected" : "legal-history-item",
|
|
91
|
+
children: [
|
|
92
|
+
"v",
|
|
93
|
+
v.version,
|
|
94
|
+
" (",
|
|
95
|
+
v.effectiveDate,
|
|
96
|
+
")"
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
) }, v.version)) })
|
|
100
|
+
] });
|
|
101
|
+
}
|
|
102
|
+
function UpcomingNotice({
|
|
103
|
+
docTitle,
|
|
104
|
+
version,
|
|
105
|
+
onPreview,
|
|
106
|
+
className
|
|
107
|
+
}) {
|
|
108
|
+
return /* @__PURE__ */ jsx("div", { className: className ? `legal-notice ${className}` : "legal-notice", children: /* @__PURE__ */ jsxs("p", { className: "legal-notice-text", children: [
|
|
109
|
+
"\u{1F4E2} ",
|
|
110
|
+
/* @__PURE__ */ jsx("b", { children: docTitle }),
|
|
111
|
+
" \uBCC0\uACBD \uC608\uC815 \u2014 ",
|
|
112
|
+
/* @__PURE__ */ jsx("b", { children: version.effectiveDate }),
|
|
113
|
+
" \uC2DC\uD589 (v",
|
|
114
|
+
version.version,
|
|
115
|
+
").",
|
|
116
|
+
version.summary ? ` ${version.summary}` : "",
|
|
117
|
+
version.adverse ? " \uC774\uC6A9\uC790\uC5D0\uAC8C \uBD88\uB9AC/\uC911\uB300\uD55C \uBCC0\uACBD\uC73C\uB85C \uC2DC\uD589 30\uC77C \uC804\uBD80\uD130 \uACF5\uC9C0\uD569\uB2C8\uB2E4." : ` (\uACF5\uACE0\uC77C ${version.publishedDate})`,
|
|
118
|
+
onPreview && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
119
|
+
" ",
|
|
120
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "legal-notice-preview", onClick: onPreview, children: "\uBCC0\uACBD \uC608\uC815\uBCF8 \uBCF4\uAE30" })
|
|
121
|
+
] })
|
|
122
|
+
] }) });
|
|
123
|
+
}
|
|
124
|
+
var defaultRenderLink = (href, children) => /* @__PURE__ */ jsx("a", { href, children });
|
|
125
|
+
function PolicyNotice({
|
|
126
|
+
docs,
|
|
127
|
+
hrefFor,
|
|
128
|
+
today,
|
|
129
|
+
renderLink = defaultRenderLink,
|
|
130
|
+
storage,
|
|
131
|
+
className
|
|
132
|
+
}) {
|
|
133
|
+
const store = storage ?? (typeof localStorage !== "undefined" ? localStorage : void 0);
|
|
134
|
+
const pending = docs.filter((doc) => doc.visibility === "public").map((doc) => ({ doc, up: upcomingVersion(doc.versions, today) })).filter((x) => x.up != null);
|
|
135
|
+
const dismissKey = "legal-policy-notice:" + pending.map((x) => `${x.doc.serviceId}:${x.doc.kind}@${x.up.version}`).join(",");
|
|
136
|
+
const [dismissed, setDismissed] = useState(() => store?.getItem(dismissKey) === "1");
|
|
137
|
+
if (pending.length === 0 || dismissed) return null;
|
|
138
|
+
return /* @__PURE__ */ jsxs("div", { className: className ? `legal-policy-notice ${className}` : "legal-policy-notice", children: [
|
|
139
|
+
/* @__PURE__ */ jsxs("span", { className: "legal-policy-notice-text", children: [
|
|
140
|
+
"\u{1F4E2}",
|
|
141
|
+
" ",
|
|
142
|
+
pending.map((x, i) => /* @__PURE__ */ jsxs("span", { children: [
|
|
143
|
+
i > 0 ? " \xB7 " : "",
|
|
144
|
+
renderLink(hrefFor(x.doc), x.doc.title),
|
|
145
|
+
" ",
|
|
146
|
+
x.up.effectiveDate,
|
|
147
|
+
"\uBD80\uD130 \uBCC0\uACBD \uC608\uC815"
|
|
148
|
+
] }, `${x.doc.serviceId}:${x.doc.kind}`))
|
|
149
|
+
] }),
|
|
150
|
+
/* @__PURE__ */ jsx(
|
|
151
|
+
"button",
|
|
152
|
+
{
|
|
153
|
+
type: "button",
|
|
154
|
+
className: "legal-policy-notice-dismiss",
|
|
155
|
+
"aria-label": "\uB2EB\uAE30",
|
|
156
|
+
onClick: () => {
|
|
157
|
+
store?.setItem(dismissKey, "1");
|
|
158
|
+
setDismissed(true);
|
|
159
|
+
},
|
|
160
|
+
children: "\u2715"
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
] });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { LegalDocument, PolicyNotice, UpcomingNotice, VersionDiff, VersionHistory, renderHighlighted };
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Optional default theme for @etamong-lab/legal.
|
|
3
|
+
*
|
|
4
|
+
* Import once in the app: `import "@etamong-lab/legal/styles.css";`
|
|
5
|
+
* Override the CSS variables to theme (including dark mode) — e.g.
|
|
6
|
+
* .dark { --legal-border: #3f3f46; --legal-muted: #a1a1aa; ... }
|
|
7
|
+
* Or skip this file entirely and style the `legal-*` classes yourself.
|
|
8
|
+
*/
|
|
9
|
+
:root {
|
|
10
|
+
--legal-fg: #3f3f46;
|
|
11
|
+
--legal-muted: #71717a;
|
|
12
|
+
--legal-border: #e4e4e7;
|
|
13
|
+
--legal-surface: #fafafa;
|
|
14
|
+
--legal-accent: #2563eb;
|
|
15
|
+
--legal-mark-bg: #fef08a;
|
|
16
|
+
--legal-mark-fg: inherit;
|
|
17
|
+
--legal-notice-bg: #eff6ff;
|
|
18
|
+
--legal-notice-border: #bfdbfe;
|
|
19
|
+
--legal-notice-fg: #1e40af;
|
|
20
|
+
--legal-badge-added-bg: #dcfce7;
|
|
21
|
+
--legal-badge-added-fg: #15803d;
|
|
22
|
+
--legal-badge-removed-bg: #fee2e2;
|
|
23
|
+
--legal-badge-removed-fg: #b91c1c;
|
|
24
|
+
--legal-badge-changed-bg: #fef3c7;
|
|
25
|
+
--legal-badge-changed-fg: #b45309;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.legal-document {
|
|
29
|
+
color: var(--legal-fg);
|
|
30
|
+
font-size: 0.875rem;
|
|
31
|
+
line-height: 1.7;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 1.5rem;
|
|
35
|
+
}
|
|
36
|
+
.legal-section-title {
|
|
37
|
+
font-size: 1rem;
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
margin: 0 0 0.5rem;
|
|
40
|
+
}
|
|
41
|
+
.legal-section-body > * + * {
|
|
42
|
+
margin-top: 0.5rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.legal-mark {
|
|
46
|
+
background: var(--legal-mark-bg);
|
|
47
|
+
color: var(--legal-mark-fg);
|
|
48
|
+
border-radius: 0.25rem;
|
|
49
|
+
padding: 0 0.125rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* --- diff table --- */
|
|
53
|
+
.legal-diff {
|
|
54
|
+
margin-top: 2.5rem;
|
|
55
|
+
}
|
|
56
|
+
.legal-diff-title {
|
|
57
|
+
font-size: 0.875rem;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
margin: 0 0 0.75rem;
|
|
60
|
+
}
|
|
61
|
+
.legal-diff-scroll {
|
|
62
|
+
overflow-x: auto;
|
|
63
|
+
border: 1px solid var(--legal-border);
|
|
64
|
+
border-radius: 0.5rem;
|
|
65
|
+
}
|
|
66
|
+
.legal-diff-table {
|
|
67
|
+
width: 100%;
|
|
68
|
+
border-collapse: collapse;
|
|
69
|
+
font-size: 0.875rem;
|
|
70
|
+
}
|
|
71
|
+
.legal-diff-table th {
|
|
72
|
+
text-align: left;
|
|
73
|
+
font-weight: 500;
|
|
74
|
+
color: var(--legal-muted);
|
|
75
|
+
background: var(--legal-surface);
|
|
76
|
+
padding: 0.5rem 0.75rem;
|
|
77
|
+
border-bottom: 1px solid var(--legal-border);
|
|
78
|
+
}
|
|
79
|
+
.legal-diff-table td {
|
|
80
|
+
vertical-align: top;
|
|
81
|
+
color: var(--legal-muted);
|
|
82
|
+
padding: 0.625rem 0.75rem;
|
|
83
|
+
border-top: 1px solid var(--legal-border);
|
|
84
|
+
}
|
|
85
|
+
.legal-diff-section span:first-child {
|
|
86
|
+
color: var(--legal-fg);
|
|
87
|
+
}
|
|
88
|
+
.legal-diff-empty {
|
|
89
|
+
opacity: 0.5;
|
|
90
|
+
}
|
|
91
|
+
.legal-badge {
|
|
92
|
+
display: inline-block;
|
|
93
|
+
margin-left: 0.375rem;
|
|
94
|
+
padding: 0.0625rem 0.375rem;
|
|
95
|
+
font-size: 0.75rem;
|
|
96
|
+
font-weight: 500;
|
|
97
|
+
border-radius: 0.25rem;
|
|
98
|
+
}
|
|
99
|
+
.legal-badge--added {
|
|
100
|
+
background: var(--legal-badge-added-bg);
|
|
101
|
+
color: var(--legal-badge-added-fg);
|
|
102
|
+
}
|
|
103
|
+
.legal-badge--removed {
|
|
104
|
+
background: var(--legal-badge-removed-bg);
|
|
105
|
+
color: var(--legal-badge-removed-fg);
|
|
106
|
+
}
|
|
107
|
+
.legal-badge--changed {
|
|
108
|
+
background: var(--legal-badge-changed-bg);
|
|
109
|
+
color: var(--legal-badge-changed-fg);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* --- version history --- */
|
|
113
|
+
.legal-history {
|
|
114
|
+
margin-top: 3rem;
|
|
115
|
+
border-top: 1px solid var(--legal-border);
|
|
116
|
+
padding-top: 1.5rem;
|
|
117
|
+
}
|
|
118
|
+
.legal-history-title {
|
|
119
|
+
font-size: 0.875rem;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
margin: 0 0 0.75rem;
|
|
122
|
+
}
|
|
123
|
+
.legal-history-list {
|
|
124
|
+
list-style: none;
|
|
125
|
+
margin: 0;
|
|
126
|
+
padding: 0;
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
gap: 0.5rem;
|
|
130
|
+
}
|
|
131
|
+
.legal-history-item {
|
|
132
|
+
background: none;
|
|
133
|
+
border: none;
|
|
134
|
+
padding: 0;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
font-size: 0.875rem;
|
|
137
|
+
color: var(--legal-accent);
|
|
138
|
+
}
|
|
139
|
+
.legal-history-item.is-selected {
|
|
140
|
+
color: var(--legal-fg);
|
|
141
|
+
font-weight: 500;
|
|
142
|
+
cursor: default;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* --- per-doc upcoming notice --- */
|
|
146
|
+
.legal-notice {
|
|
147
|
+
background: var(--legal-notice-bg);
|
|
148
|
+
border: 1px solid var(--legal-notice-border);
|
|
149
|
+
border-radius: 0.5rem;
|
|
150
|
+
padding: 0.75rem 1rem;
|
|
151
|
+
margin-bottom: 1.5rem;
|
|
152
|
+
}
|
|
153
|
+
.legal-notice-text {
|
|
154
|
+
margin: 0;
|
|
155
|
+
font-size: 0.875rem;
|
|
156
|
+
color: var(--legal-notice-fg);
|
|
157
|
+
}
|
|
158
|
+
.legal-notice-preview {
|
|
159
|
+
background: none;
|
|
160
|
+
border: none;
|
|
161
|
+
padding: 0;
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
font-weight: 500;
|
|
164
|
+
text-decoration: underline;
|
|
165
|
+
color: inherit;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* --- site-wide policy banner --- */
|
|
169
|
+
.legal-policy-notice {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
justify-content: space-between;
|
|
173
|
+
gap: 0.75rem;
|
|
174
|
+
background: var(--legal-notice-bg);
|
|
175
|
+
border-bottom: 1px solid var(--legal-notice-border);
|
|
176
|
+
color: var(--legal-notice-fg);
|
|
177
|
+
padding: 0.5rem 1rem;
|
|
178
|
+
font-size: 0.875rem;
|
|
179
|
+
}
|
|
180
|
+
.legal-policy-notice a {
|
|
181
|
+
color: inherit;
|
|
182
|
+
font-weight: 500;
|
|
183
|
+
}
|
|
184
|
+
.legal-policy-notice-dismiss {
|
|
185
|
+
background: none;
|
|
186
|
+
border: none;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
color: inherit;
|
|
189
|
+
font-size: 1rem;
|
|
190
|
+
line-height: 1;
|
|
191
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@etamong-playground/legal",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Shared versioned legal-document model + headless renderers (Terms/Privacy, version diff, advance-notice) for etamong-lab apps.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"*.css"
|
|
8
|
+
],
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
},
|
|
18
|
+
"./helpers": {
|
|
19
|
+
"types": "./dist/helpers.d.ts",
|
|
20
|
+
"import": "./dist/helpers.js",
|
|
21
|
+
"require": "./dist/helpers.cjs"
|
|
22
|
+
},
|
|
23
|
+
"./styles.css": "./dist/styles.css"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"dev": "tsup --watch",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^18.3.12",
|
|
38
|
+
"react": "^18.3.1",
|
|
39
|
+
"tsup": "^8.3.5",
|
|
40
|
+
"typescript": "^5.6.3"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"license": "UNLICENSED"
|
|
46
|
+
}
|