@madeinbruno/atelier 0.4.3 → 0.6.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 CHANGED
@@ -14,7 +14,7 @@ React 18 이상이 필요해요.
14
14
 
15
15
  ### `Tag` — 공용 푸터
16
16
 
17
- 흰 옷 택(행택) 모양의 푸터예요. 택 위쪽에 한 줄 박음질이 있고, 픽셀 글씨 `madeinbruno`, `SERVICE / CONTACT`와 madeinbruno.com 링크가 들어가요.
17
+ 흰 옷 택(행택) 모양의 푸터예요. 택 위쪽에 한 줄 박음질이 있고, 픽셀 글씨 `madeinbruno`, `SERVICE / CONTACT`가 들어가요 (`href`를 주면 밑줄 친 링크도).
18
18
 
19
19
  ```tsx
20
20
  import { Tag } from '@madeinbruno/atelier';
@@ -29,11 +29,37 @@ export default function Footer() {
29
29
  |---|---|---|
30
30
  | `service` (필수) | | 택의 SERVICE 줄. 서비스 이름 |
31
31
  | `email` | `pbk575@gmail.com` | CONTACT 줄 (mailto 링크) |
32
- | `href` | `https://madeinbruno.com` | 택 아래 링크 (밑줄 친 주소) |
32
+ | `href` | 없음 | 택 아래 링크 (밑줄 친 주소). 주지 않으면 링크를 그리지 않아요. madeinbruno.com이 생기면 기본값으로 넣을 예정 |
33
33
  | `since` | `EST. 2026` | 택 오른쪽 위 작은 글씨 |
34
34
  | `eyebrow`, `message` | "Thanks for stopping by", "이 사이트도 madeinbruno에서 만들었어요." | 택 왼쪽 글. 둘 다 비우면 택만 가운데에 |
35
35
  | `background` | `#f4f1ea` | 푸터 바탕. 서비스 바탕색에 맞추기 |
36
36
  | `stitch` | `#9a948b` (회색) | 박음질 색 |
37
+ | `care` | 없음 | 택 아래 세탁 표시 줄. `true`면 기본 상태, `CareState`를 주면 그 상태로. 아래 참고 |
38
+
39
+ #### 세탁 표시 (`care`)
40
+
41
+ 옷 택의 세탁 기호를 원래 모양 그대로 그리고, 몇몇 기호에 만든 사람의 지금 상태를 숨겨 둬요. 마우스를 올리면 짧은 영어 문장이 떠요.
42
+
43
+ | 기호 | 숨은 뜻 | 툴팁 |
44
+ |---|---|---|
45
+ | 물세탁 빨래통 | 온도 자리 = 나이 (올해 − 2005) | he is 21 |
46
+ | 표백 금지 | `quit`: 끊고 있는 것. 시작한 날(`since`, YYYY-MM-DD 한국 날짜)부터 며칠째인지 숫자가 ✕ 자리에. 없으면 ✕ | he is on day 12 without ○○ |
47
+ | 건조 네모 | `wet`: 마음. 기호는 늘 텀블 건조 그대로, 켜 두면 툴팁만 | still not dry (켰을 때만) |
48
+ | 다리미 | `heat` 1~3 점: 뜨거울수록 열심히 하는 중 | he is taking it easy / busy / on fire |
49
+ | 드라이클리닝 | `interest`: 요즘 관심 있는 것. `since`부터 1주일 동안 동그라미 안에 `icon`(고른 아이콘), 지나거나 없으면 ✕ | he is into ○○ (1주일 동안만) |
50
+
51
+ ```tsx
52
+ <Tag
53
+ service="blog"
54
+ care={{
55
+ heat: 3,
56
+ interest: { text: '필름 카메라', icon: 'camera', since: '2026-11-03T00:00:00Z' },
57
+ quit: { text: '커피', since: '2026-10-23' },
58
+ }}
59
+ />
60
+ ```
61
+
62
+ 설정 화면에서 미리 보기용으로 `CareLabel`만 따로 쓸 수도 있어요 (`.mib-tag__care` 스타일 포함). 관심사가 아직 보이는 중인지는 `activeInterest(interest)`, 끊은 지 며칠째인지는 `quitDay(quit)`로 알 수 있어요. 고를 수 있는 아이콘은 `CARE_ICONS`에 있어요 (음악, 영화, 책, 사진, 게임, 코드, 커피, 식물, 운동, 글, 마음).
37
63
 
38
64
  `madeinbruno` 글씨는 [Banquise](https://gitlab.com/bonjour-monde/fonderie/banquise-typeface)(Bonjour Monde, SIL Open Font License 1.1)로 쓴 글자를 SVG 그림으로 옮긴 거예요. 이 한 단어에만 쓰는 폰트라 폰트 파일을 싣지 않아요 (15KB → 1KB 미만, 글씨가 늦게 바뀌는 깜빡임도 없음).
39
65
 
package/dist/index.d.ts CHANGED
@@ -1,12 +1,110 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
+ /**
5
+ * 드라이클리닝 동그라미 안에 넣는 관심사 아이콘. 24×24 선 그림.
6
+ * 동그라미 안에서는 작게 줄여 그린다 (CareLabel).
7
+ */
8
+ declare const CARE_ICONS: {
9
+ music: {
10
+ label: string;
11
+ path: react.JSX.Element;
12
+ };
13
+ film: {
14
+ label: string;
15
+ path: react.JSX.Element;
16
+ };
17
+ book: {
18
+ label: string;
19
+ path: react.JSX.Element;
20
+ };
21
+ camera: {
22
+ label: string;
23
+ path: react.JSX.Element;
24
+ };
25
+ game: {
26
+ label: string;
27
+ path: react.JSX.Element;
28
+ };
29
+ code: {
30
+ label: string;
31
+ path: react.JSX.Element;
32
+ };
33
+ coffee: {
34
+ label: string;
35
+ path: react.JSX.Element;
36
+ };
37
+ plant: {
38
+ label: string;
39
+ path: react.JSX.Element;
40
+ };
41
+ ball: {
42
+ label: string;
43
+ path: react.JSX.Element;
44
+ };
45
+ pencil: {
46
+ label: string;
47
+ path: react.JSX.Element;
48
+ };
49
+ heart: {
50
+ label: string;
51
+ path: react.JSX.Element;
52
+ };
53
+ };
54
+ type CareIcon = keyof typeof CARE_ICONS;
55
+ declare function isCareIcon(v: unknown): v is CareIcon;
56
+
57
+ /**
58
+ * 옷 택의 세탁 표시 줄 (ISO 3758 기호를 원래 모양 그대로).
59
+ * 몇몇 기호에 만든 사람의 지금 상태가 숨어 있고, 마우스를 올리면 짧은 영어 문장이 뜬다.
60
+ *
61
+ * 물세탁 빨래통 온도 자리 = 나이 (올해 − 2005)
62
+ * 표백 금지 끊고 있는 것. 깨끗하게(표백) 끊은 지 며칠째인지 숫자가 ✕ 자리에. 없으면 ✕
63
+ * 건조 네모 마음. 기호는 늘 텀블 건조 그대로, 켜 두면 툴팁만 뜬다
64
+ * 다리미 점 1~3 = 다리미가 뜨거울수록 무언가를 열심히 하는 중
65
+ * 드라이클리닝 요즘 관심 있는 것. 정한 뒤 1주일 동안 동그라미 안에 고른 아이콘, 지나면 ✕
66
+ */
67
+ type CareState = {
68
+ /** 얼마나 열심히 하는 중인지 (다리미 점 개수) */
69
+ heat?: 1 | 2 | 3;
70
+ /** 요즘 관심 있는 것과 정한 때. since부터 1주일 동안만 보인다. icon은 동그라미 안에 그린다 */
71
+ interest?: {
72
+ text: string;
73
+ since: string;
74
+ icon?: CareIcon | null;
75
+ } | null;
76
+ /** 끊고 있는 것과 시작한 날(YYYY-MM-DD, 한국 날짜). 시작한 날이 1일째 */
77
+ quit?: {
78
+ text: string;
79
+ since: string;
80
+ } | null;
81
+ /** 아직 마르지 않은 마음. 켜 두면 건조 네모에 툴팁만 뜬다 (기호는 그대로) */
82
+ wet?: boolean;
83
+ };
84
+ declare const DEFAULT_CARE_STATE: Required<CareState>;
85
+ /** 관심사가 보이는 기간 */
86
+ declare const INTEREST_DAYS = 7;
87
+ /** 관심사가 아직 1주일 안인지. 지났거나 없으면 null */
88
+ declare function activeInterest(interest: CareState['interest'], now?: number): {
89
+ text: string;
90
+ icon: "music" | "film" | "book" | "camera" | "game" | "code" | "coffee" | "plant" | "ball" | "pencil" | "heart" | null;
91
+ until: Date;
92
+ } | null;
93
+ /** 한국 날짜로 오늘 (YYYY-MM-DD) */
94
+ declare function todayInKorea(now?: number): string;
95
+ /** 끊은 지 며칠째인지 (시작한 날 = 1일째). 없거나 날짜가 이상하거나 아직 시작 전이면 null */
96
+ declare function quitDay(quit: CareState['quit'], now?: number): number | null;
97
+ declare function CareLabel({ state }: {
98
+ state?: CareState;
99
+ }): react.JSX.Element;
100
+
4
101
  type TagProps = {
5
102
  /** 이 서비스의 이름. 택의 SERVICE 줄에 들어간다 (예: "hangoutwithme"). */
6
103
  service: string;
7
104
  /** 연락받을 이메일. 택의 CONTACT 줄에 mailto 링크로 들어간다. */
8
105
  email?: string;
9
- /** 택 아래 링크(밑줄 친 주소)가 가는 곳. */
106
+ /** 택 아래 링크(밑줄 친 주소)가 가는 곳. 주지 않으면 링크를 그리지 않는다.
107
+ * madeinbruno.com이 아직 없어서 기본은 링크 없음. 사이트가 생기면 기본값을 'https://madeinbruno.com'으로 바꾼다. */
10
108
  href?: string;
11
109
  /** 택 오른쪽 위의 작은 글씨. */
12
110
  since?: string;
@@ -17,8 +115,11 @@ type TagProps = {
17
115
  background?: string;
18
116
  /** 택 위쪽 박음질 색 (기본: 회색). */
19
117
  stitch?: string;
118
+ /** 택 아래 세탁 표시 줄. true면 기본 상태로, 객체를 주면 그 상태(끊고 있는 것·마음·열정·요즘 관심사)로 그린다.
119
+ * 기호에 마우스를 올리면 짧은 영어 문장이 뜬다. */
120
+ care?: boolean | CareState;
20
121
  className?: string;
21
122
  };
22
- declare function Tag({ service, email, href, since, eyebrow, message, background, stitch, className, }: TagProps): react.JSX.Element;
123
+ declare function Tag({ service, email, href, since, eyebrow, message, background, stitch, care, className, }: TagProps): react.JSX.Element;
23
124
 
24
- export { Tag, type TagProps };
125
+ export { CARE_ICONS, type CareIcon, CareLabel, type CareState, DEFAULT_CARE_STATE, INTEREST_DAYS, Tag, type TagProps, activeInterest, isCareIcon, quitDay, todayInKorea };
package/dist/index.js CHANGED
@@ -1,15 +1,171 @@
1
+ // src/careIcons.tsx
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ var CARE_ICONS = {
4
+ music: {
5
+ label: "\uC74C\uC545",
6
+ path: /* @__PURE__ */ jsxs(Fragment, { children: [
7
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "17", r: "3" }),
8
+ /* @__PURE__ */ jsx("path", { d: "M11 17 V5 L19 7.5 V10 L11 7.5" })
9
+ ] })
10
+ },
11
+ film: {
12
+ label: "\uC601\uD654",
13
+ path: /* @__PURE__ */ jsxs(Fragment, { children: [
14
+ /* @__PURE__ */ jsx("rect", { x: "4", y: "5", width: "16", height: "14", rx: "1" }),
15
+ /* @__PURE__ */ jsx("path", { d: "M8 5 V19 M16 5 V19 M4 12 H8 M16 12 H20" })
16
+ ] })
17
+ },
18
+ book: {
19
+ label: "\uCC45",
20
+ path: /* @__PURE__ */ jsxs(Fragment, { children: [
21
+ /* @__PURE__ */ jsx("path", { d: "M3 6 Q7.5 4 12 6 Q16.5 4 21 6 V19 Q16.5 17 12 19 Q7.5 17 3 19 Z" }),
22
+ /* @__PURE__ */ jsx("path", { d: "M12 6 V19" })
23
+ ] })
24
+ },
25
+ camera: {
26
+ label: "\uC0AC\uC9C4",
27
+ path: /* @__PURE__ */ jsxs(Fragment, { children: [
28
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "8", width: "18", height: "11", rx: "1.5" }),
29
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "13.5", r: "3.5" }),
30
+ /* @__PURE__ */ jsx("path", { d: "M8 8 L9.5 5.5 H14.5 L16 8" })
31
+ ] })
32
+ },
33
+ game: {
34
+ label: "\uAC8C\uC784",
35
+ path: /* @__PURE__ */ jsxs(Fragment, { children: [
36
+ /* @__PURE__ */ jsx("rect", { x: "2.5", y: "7.5", width: "19", height: "10", rx: "5" }),
37
+ /* @__PURE__ */ jsx("path", { d: "M7.5 10.5 V14.5 M5.5 12.5 H9.5" }),
38
+ /* @__PURE__ */ jsx("circle", { cx: "15.5", cy: "11.5", r: "0.9" }),
39
+ /* @__PURE__ */ jsx("circle", { cx: "18", cy: "13.5", r: "0.9" })
40
+ ] })
41
+ },
42
+ code: {
43
+ label: "\uCF54\uB4DC",
44
+ path: /* @__PURE__ */ jsx("path", { d: "M8 7 L3 12 L8 17 M16 7 L21 12 L16 17 M13.5 5 L10.5 19" })
45
+ },
46
+ coffee: {
47
+ label: "\uCEE4\uD53C",
48
+ path: /* @__PURE__ */ jsxs(Fragment, { children: [
49
+ /* @__PURE__ */ jsx("path", { d: "M4.5 9 H16.5 V14.5 A4.5 4.5 0 0 1 12 19 H9 A4.5 4.5 0 0 1 4.5 14.5 Z" }),
50
+ /* @__PURE__ */ jsx("path", { d: "M16.5 10.5 H18 A2.2 2.2 0 0 1 18 15 H16.5" }),
51
+ /* @__PURE__ */ jsx("path", { d: "M8.5 3.5 V6 M12.5 3.5 V6" })
52
+ ] })
53
+ },
54
+ plant: {
55
+ label: "\uC2DD\uBB3C",
56
+ path: /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ /* @__PURE__ */ jsx("path", { d: "M12 21 V11" }),
58
+ /* @__PURE__ */ jsx("path", { d: "M12 14 C7 14 4.5 10 4.5 6 C9 6 12 9 12 14 Z" }),
59
+ /* @__PURE__ */ jsx("path", { d: "M12 11 C12 7 14.5 4 19.5 4 C19.5 8 17 11 12 11 Z" })
60
+ ] })
61
+ },
62
+ ball: {
63
+ label: "\uC6B4\uB3D9",
64
+ path: /* @__PURE__ */ jsxs(Fragment, { children: [
65
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "8.5" }),
66
+ /* @__PURE__ */ jsx("path", { d: "M3.5 12 H20.5 M12 3.5 C8.5 7 8.5 17 12 20.5 M12 3.5 C15.5 7 15.5 17 12 20.5" })
67
+ ] })
68
+ },
69
+ pencil: {
70
+ label: "\uAE00",
71
+ path: /* @__PURE__ */ jsxs(Fragment, { children: [
72
+ /* @__PURE__ */ jsx("path", { d: "M4 20 L5 15 L15.5 4.5 L19.5 8.5 L9 19 Z" }),
73
+ /* @__PURE__ */ jsx("path", { d: "M13.5 6.5 L17.5 10.5" })
74
+ ] })
75
+ },
76
+ heart: {
77
+ label: "\uB9C8\uC74C",
78
+ path: /* @__PURE__ */ jsx("path", { d: "M12 20 C5 14.5 3 11.5 3 8.5 A4.5 4.5 0 0 1 12 7 A4.5 4.5 0 0 1 21 8.5 C21 11.5 19 14.5 12 20 Z" })
79
+ }
80
+ };
81
+ function isCareIcon(v) {
82
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(CARE_ICONS, v);
83
+ }
84
+
85
+ // src/CareLabel.tsx
86
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
87
+ var DEFAULT_CARE_STATE = {
88
+ heat: 2,
89
+ interest: null,
90
+ quit: null,
91
+ wet: false
92
+ };
93
+ var INTEREST_DAYS = 7;
94
+ var BORN = 2005;
95
+ var DAY = 24 * 60 * 60 * 1e3;
96
+ var HEAT_TIP = { 1: "he is taking it easy", 2: "he is busy", 3: "he is on fire" };
97
+ function activeInterest(interest, now = Date.now()) {
98
+ if (!interest?.text) return null;
99
+ const since = Date.parse(interest.since);
100
+ if (Number.isNaN(since) || now - since > INTEREST_DAYS * DAY) return null;
101
+ return {
102
+ text: interest.text,
103
+ icon: isCareIcon(interest.icon) ? interest.icon : null,
104
+ until: new Date(since + INTEREST_DAYS * DAY)
105
+ };
106
+ }
107
+ function todayInKorea(now = Date.now()) {
108
+ return new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Seoul" }).format(now);
109
+ }
110
+ function quitDay(quit, now = Date.now()) {
111
+ if (!quit?.text || !/^\d{4}-\d{2}-\d{2}$/.test(quit.since)) return null;
112
+ const start = Date.parse(`${quit.since}T00:00:00Z`);
113
+ const today = Date.parse(`${todayInKorea(now)}T00:00:00Z`);
114
+ const day = Math.round((today - start) / DAY) + 1;
115
+ return day >= 1 ? day : null;
116
+ }
117
+ function Dot({ cx, cy }) {
118
+ return /* @__PURE__ */ jsx2("circle", { cx, cy, r: "1", className: "mib-tag__care-dot" });
119
+ }
120
+ function Symbol({ tip, children }) {
121
+ return /* @__PURE__ */ jsx2("li", { className: "mib-tag__care-item", "data-tip": tip, "aria-label": tip, "aria-hidden": tip ? void 0 : true, children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children }) });
122
+ }
123
+ function CareLabel({ state }) {
124
+ const s = { ...DEFAULT_CARE_STATE, ...state };
125
+ const age = (/* @__PURE__ */ new Date()).getFullYear() - BORN;
126
+ const dots = s.heat === 1 ? [12.5] : s.heat === 2 ? [10.5, 14.5] : [9, 12.5, 16];
127
+ const interest = activeInterest(s.interest);
128
+ const quit = quitDay(s.quit);
129
+ return /* @__PURE__ */ jsxs2("ul", { className: "mib-tag__care", "aria-label": "care label", children: [
130
+ /* @__PURE__ */ jsxs2(Symbol, { tip: `he is ${age}`, children: [
131
+ /* @__PURE__ */ jsx2("path", { d: "M2.5 7.5 q2.4 -2 4.75 0 t4.75 0 t4.75 0 t4.75 0" }),
132
+ /* @__PURE__ */ jsx2("path", { d: "M3.2 8.1 L5.4 19.5 H18.6 L20.8 8.1" }),
133
+ /* @__PURE__ */ jsx2("text", { x: "12", y: "16.6", textAnchor: "middle", children: age })
134
+ ] }),
135
+ /* @__PURE__ */ jsxs2(Symbol, { tip: quit && s.quit ? `he is on day ${quit} without ${s.quit.text}` : void 0, children: [
136
+ /* @__PURE__ */ jsx2("path", { d: "M12 3.5 L21 19.5 H3 Z" }),
137
+ quit ? /* @__PURE__ */ jsx2("text", { x: "12", y: "17.6", textAnchor: "middle", className: "mib-tag__care-small", children: quit }) : /* @__PURE__ */ jsx2("path", { d: "M7 8.5 L17 19.5 M17 8.5 L7 19.5" })
138
+ ] }),
139
+ /* @__PURE__ */ jsxs2(Symbol, { tip: s.wet ? "still not dry" : void 0, children: [
140
+ /* @__PURE__ */ jsx2("rect", { x: "3.5", y: "3.5", width: "17", height: "17" }),
141
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "6" }),
142
+ /* @__PURE__ */ jsx2(Dot, { cx: 12, cy: 12 })
143
+ ] }),
144
+ /* @__PURE__ */ jsxs2(Symbol, { tip: HEAT_TIP[s.heat], children: [
145
+ /* @__PURE__ */ jsx2("path", { d: "M3 18 H21 C21 12.5 18.5 9.5 13.5 9.5 H7.5 C5.2 9.5 3.6 12.8 3 18 Z" }),
146
+ dots.map((x) => /* @__PURE__ */ jsx2(Dot, { cx: x, cy: 14.2 }, x))
147
+ ] }),
148
+ /* @__PURE__ */ jsxs2(Symbol, { tip: interest ? `he is into ${interest.text}` : void 0, children: [
149
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "8" }),
150
+ !interest && /* @__PURE__ */ jsx2("path", { d: "M5.5 5.5 L18.5 18.5 M18.5 5.5 L5.5 18.5" }),
151
+ interest?.icon && /* @__PURE__ */ jsx2("g", { className: "mib-tag__care-icon", transform: "translate(12 12) scale(0.42) translate(-12 -12)", children: CARE_ICONS[interest.icon].path })
152
+ ] })
153
+ ] });
154
+ }
155
+
1
156
  // src/Tag.tsx
2
- import { jsx, jsxs } from "react/jsx-runtime";
157
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
3
158
  var WORDMARK = "M18 0h2v2h-2zM27 0h2v1h-2zM36 0h2v2h-2zM0 2h1v1h-1zM2 2h2v1h-2zM5 2h2v1h-2zM9 2h4v1h-4zM17 2h3v1h-3zM22 2h3v1h-3zM27 2h2v6h-2zM30 2h1v1h-1zM32 2h2v1h-2zM36 2h4v1h-4zM42 2h1v1h-1zM44 2h2v1h-2zM50 2h2v5h-2zM53 2h1v1h-1zM55 2h2v1h-2zM60 2h3v1h-3zM0 3h2v5h-2zM3 3h2v5h-2zM6 3h2v5h-2zM9 3h2v1h-2zM12 3h2v2h-2zM15 3h2v4h-2zM18 3h2v4h-2zM21 3h2v1h-2zM24 3h2v1h-2zM30 3h2v5h-2zM33 3h2v5h-2zM36 3h2v4h-2zM39 3h2v4h-2zM42 3h4v1h-4zM47 3h2v4h-2zM53 3h2v5h-2zM56 3h2v5h-2zM62 3h2v4h-2zM21 4h5v1h-5zM42 4h2v4h-2zM59 4h2v3h-2zM9 5h5v1h-5zM21 5h2v2h-2zM9 6h2v1h-2zM12 6h2v1h-2zM25 6h1v1h-1zM10 7h2v1h-2zM13 7h1v1h-1zM16 7h2v1h-2zM19 7h1v1h-1zM23 7h2v1h-2zM36 7h1v1h-1zM38 7h1v1h-1zM48 7h2v1h-2zM51 7h1v1h-1zM61 7h2v1h-2z";
4
159
  function Tag({
5
160
  service,
6
161
  email = "pbk575@gmail.com",
7
- href = "https://madeinbruno.com",
162
+ href,
8
163
  since = "EST. 2026",
9
164
  eyebrow = "Thanks for stopping by",
10
165
  message = `\uC774 \uC0AC\uC774\uD2B8\uB3C4 madeinbruno\uC5D0\uC11C \uB9CC\uB4E4\uC5C8\uC5B4\uC694.`,
11
166
  background,
12
167
  stitch,
168
+ care = false,
13
169
  className
14
170
  }) {
15
171
  const style = {
@@ -17,25 +173,26 @@ function Tag({
17
173
  ...stitch ? { "--mib-stitch": stitch } : {}
18
174
  };
19
175
  const hasMessage = Boolean(eyebrow || message);
20
- const linkLabel = href.replace(/^https?:\/\//, "").replace(/\/$/, "");
21
- return /* @__PURE__ */ jsx("footer", { className: ["mib-footer", className].filter(Boolean).join(" "), style, children: /* @__PURE__ */ jsxs("div", { className: hasMessage ? "mib-footer__inner" : "mib-footer__inner mib-footer__inner--solo", children: [
22
- hasMessage && /* @__PURE__ */ jsxs("div", { className: "mib-footer__message", children: [
23
- eyebrow && /* @__PURE__ */ jsx("p", { className: "mib-footer__eyebrow", children: eyebrow }),
24
- message && /* @__PURE__ */ jsx("p", { className: "mib-footer__text", children: message })
176
+ const linkLabel = href?.replace(/^https?:\/\//, "").replace(/\/$/, "");
177
+ return /* @__PURE__ */ jsx3("footer", { className: ["mib-footer", className].filter(Boolean).join(" "), style, children: /* @__PURE__ */ jsxs3("div", { className: hasMessage ? "mib-footer__inner" : "mib-footer__inner mib-footer__inner--solo", children: [
178
+ hasMessage && /* @__PURE__ */ jsxs3("div", { className: "mib-footer__message", children: [
179
+ eyebrow && /* @__PURE__ */ jsx3("p", { className: "mib-footer__eyebrow", children: eyebrow }),
180
+ message && /* @__PURE__ */ jsx3("p", { className: "mib-footer__text", children: message })
25
181
  ] }),
26
- /* @__PURE__ */ jsx("div", { className: "mib-tag-hang", children: /* @__PURE__ */ jsx("div", { className: "mib-tag", children: /* @__PURE__ */ jsxs("div", { className: "mib-tag__card", children: [
27
- /* @__PURE__ */ jsx("span", { className: "mib-tag__stitch", "aria-hidden": "true" }),
28
- /* @__PURE__ */ jsxs("div", { className: "mib-tag__head", children: [
29
- /* @__PURE__ */ jsx("svg", { className: "mib-tag__brand", viewBox: "0 0 64 8", role: "img", "aria-label": "madeinbruno", children: /* @__PURE__ */ jsx("path", { d: WORDMARK }) }),
30
- /* @__PURE__ */ jsx("span", { className: "mib-tag__since", children: since })
182
+ /* @__PURE__ */ jsx3("div", { className: "mib-tag-hang", children: /* @__PURE__ */ jsx3("div", { className: "mib-tag", children: /* @__PURE__ */ jsxs3("div", { className: "mib-tag__card", children: [
183
+ /* @__PURE__ */ jsx3("span", { className: "mib-tag__stitch", "aria-hidden": "true" }),
184
+ /* @__PURE__ */ jsxs3("div", { className: "mib-tag__head", children: [
185
+ /* @__PURE__ */ jsx3("svg", { className: "mib-tag__brand", viewBox: "0 0 64 8", role: "img", "aria-label": "madeinbruno", children: /* @__PURE__ */ jsx3("path", { d: WORDMARK }) }),
186
+ /* @__PURE__ */ jsx3("span", { className: "mib-tag__since", children: since })
31
187
  ] }),
32
- /* @__PURE__ */ jsxs("dl", { className: "mib-tag__specs", children: [
33
- /* @__PURE__ */ jsx("dt", { children: "SERVICE" }),
34
- /* @__PURE__ */ jsx("dd", { children: service }),
35
- /* @__PURE__ */ jsx("dt", { children: "CONTACT" }),
36
- /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("a", { href: `mailto:${email}`, children: email }) })
188
+ /* @__PURE__ */ jsxs3("dl", { className: "mib-tag__specs", children: [
189
+ /* @__PURE__ */ jsx3("dt", { children: "SERVICE" }),
190
+ /* @__PURE__ */ jsx3("dd", { children: service }),
191
+ /* @__PURE__ */ jsx3("dt", { children: "CONTACT" }),
192
+ /* @__PURE__ */ jsx3("dd", { children: /* @__PURE__ */ jsx3("a", { href: `mailto:${email}`, children: email }) })
37
193
  ] }),
38
- /* @__PURE__ */ jsxs("a", { className: "mib-tag__link", href, children: [
194
+ care && /* @__PURE__ */ jsx3(CareLabel, { state: care === true ? void 0 : care }),
195
+ href && /* @__PURE__ */ jsxs3("a", { className: "mib-tag__link", href, children: [
39
196
  linkLabel,
40
197
  " \u2192"
41
198
  ] })
@@ -43,5 +200,13 @@ function Tag({
43
200
  ] }) });
44
201
  }
45
202
  export {
46
- Tag
203
+ CARE_ICONS,
204
+ CareLabel,
205
+ DEFAULT_CARE_STATE,
206
+ INTEREST_DAYS,
207
+ Tag,
208
+ activeInterest,
209
+ isCareIcon,
210
+ quitDay,
211
+ todayInKorea
47
212
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madeinbruno/atelier",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "madeinbruno.com 서비스들이 같이 쓰는 공용 컴포넌트 (옷 만드는 작업실 컨셉: 택, 라벨, 도장…)",
5
5
  "license": "MIT",
6
6
  "author": "codingbotPark <pbk575@gmail.com>",
package/styles.css CHANGED
@@ -12,7 +12,7 @@
12
12
  --mib-muted: #6b655c;
13
13
  box-sizing: border-box;
14
14
  width: 100%;
15
- overflow: hidden;
15
+ /* 넘치는 걸 자르지 않는다: 자르면 택 위로 퍼지는 그림자가 푸터 윗변에서 끊겨, 푸터 자리만 바탕색이 달라 보인다 */
16
16
  padding: 0 24px;
17
17
  background: var(--mib-ground);
18
18
  font-family: 'IBM Plex Mono', ui-monospace, monospace;
@@ -92,7 +92,6 @@
92
92
  display: flex;
93
93
  flex-direction: column;
94
94
  gap: 14px;
95
- min-height: 236px;
96
95
  padding: 46px 34px 26px;
97
96
  background: #ffffff;
98
97
  /* 옷 택처럼 위쪽 두 모서리를 비스듬히 깎는다 */
@@ -149,6 +148,83 @@
149
148
  overflow-wrap: anywhere;
150
149
  }
151
150
 
151
+ .mib-tag__care {
152
+ display: flex;
153
+ flex-wrap: wrap;
154
+ gap: 14px;
155
+ margin: 0;
156
+ padding: 14px 0 0;
157
+ border-top: 1px solid #e7e1d6;
158
+ list-style: none;
159
+ color: var(--mib-ink);
160
+ }
161
+
162
+ .mib-tag__care svg {
163
+ display: block;
164
+ width: 24px;
165
+ height: 24px;
166
+ fill: none;
167
+ stroke: currentColor;
168
+ stroke-width: 1.4;
169
+ stroke-linecap: round;
170
+ stroke-linejoin: round;
171
+ }
172
+
173
+ .mib-tag__care text {
174
+ fill: currentColor;
175
+ stroke: none;
176
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
177
+ font-size: 7px;
178
+ font-weight: 500;
179
+ }
180
+
181
+ .mib-tag__care .mib-tag__care-small {
182
+ font-size: 5.5px;
183
+ }
184
+
185
+ .mib-tag__care .mib-tag__care-icon {
186
+ /* 0.42배로 줄여 그리므로 선을 굵게 둬서 다른 기호와 비슷한 굵기로 */
187
+ stroke-width: 3;
188
+ }
189
+
190
+ .mib-tag__care .mib-tag__care-dot {
191
+ fill: currentColor;
192
+ stroke: none;
193
+ }
194
+
195
+ /* 툴팁: 기호 위에 작은 글씨가 뜬다 (CSS만, 스크립트 없음).
196
+ 세탁 표시 줄이 택 왼쪽에 붙어 있으므로 기호 왼쪽 끝에서 오른쪽으로 펼쳐 택·화면 밖으로 나가지 않게 한다. */
197
+ .mib-tag__care-item {
198
+ position: relative;
199
+ display: flex;
200
+ }
201
+
202
+ .mib-tag__care-item[data-tip]::after {
203
+ content: attr(data-tip);
204
+ position: absolute;
205
+ bottom: calc(100% + 8px);
206
+ left: 0;
207
+ padding: 4px 8px;
208
+ border-radius: 4px;
209
+ background: var(--mib-ink);
210
+ color: #fff;
211
+ font-size: 11px;
212
+ line-height: 1.3;
213
+ white-space: nowrap;
214
+ max-width: 240px;
215
+ overflow: hidden;
216
+ text-overflow: ellipsis;
217
+ pointer-events: none;
218
+ opacity: 0;
219
+ transform: translateY(2px);
220
+ transition: opacity 120ms ease, transform 120ms ease;
221
+ }
222
+
223
+ .mib-tag__care-item[data-tip]:hover::after {
224
+ opacity: 1;
225
+ transform: translateY(0);
226
+ }
227
+
152
228
  .mib-tag__link {
153
229
  align-self: flex-start;
154
230
  margin-top: auto;
@@ -183,7 +259,6 @@
183
259
  }
184
260
 
185
261
  .mib-tag__card {
186
- min-height: 0;
187
262
  padding: 44px 24px 24px;
188
263
  }
189
264
  }