social_login_buttons 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2292c0fb0d3070a21f520435e4e03d3f7188f20372b463f1a646d1f084dbf8bd
4
+ data.tar.gz: bd8f5ce236d1efdcb4eb1a8efe0aa921a203f086a22f6357b9e094dad94b276f
5
+ SHA512:
6
+ metadata.gz: 20337a14821d7336087fc883c278c7ea46eb94061e47ed440404f26fc938e106935f20b7877d4d955a6716922d7ad074022d293e57067c3df0dd3cd134079465
7
+ data.tar.gz: 32d0485dd06ab014e416d15f64861edf57b33a06553c61acc8cafec8530673fb898caeb7b6b5d610f4484f8779b476f27ef99355dbf33a956bcd3598b27e453f
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dico (AI-Dico)
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.
22
+
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # social_login_buttons
2
+
3
+ Version 0.1.0 · Ruby >=3.1 · no runtime dependencies beyond standard-library JSON.
4
+ Kakao, NAVER, Apple and Google buttons with HTML identical to
5
+ `@ai-dico/social-login-buttons` for the same options.
6
+
7
+ ```ruby
8
+ gem 'social_login_buttons', '~> 0.1.0'
9
+ ```
10
+
11
+ ```erb
12
+ <%= stylesheet_link_tag 'social_login_buttons' %>
13
+ <%= social_login_button :kakao, purpose: :continue %>
14
+ <%= social_login_button :naver, type: :submit, attrs: { 'name' => 'provider', 'value' => 'naver' } %>
15
+ ```
16
+
17
+ The Railtie registers the helper with ActionView and the stylesheet directory with
18
+ an available Rails asset pipeline. No Rails installation is needed for plain Ruby:
19
+
20
+ ```ruby
21
+ require 'social_login_buttons'
22
+ html = SocialLoginButtons.render(provider: :naver, lang: :en)
23
+ ```
24
+
25
+ Plain render returns a String. Only the helper calls `html_safe` when available.
26
+ Options accept symbol or string keys and values, and both `logoColor`/`logo_color`
27
+ and `appleLogoSrc`/`apple_logo_src`. Attribute maps use string keys and string values.
28
+
29
+ | Option | Values | Default |
30
+ | --- | --- | --- |
31
+ | provider | kakao, naver, apple, google | required |
32
+ | purpose | login, continue, signup, short | continue |
33
+ | lang | ko, en | ko |
34
+ | tone | brand, white, black, custom | brand |
35
+ | theme | light, dark | light |
36
+ | corner | official, 0, 4, 8, 12, pill | official |
37
+ | height | official or a positive finite pixel number | 48 |
38
+ | font | unified, brand | unified |
39
+ | logoColor | brand, mono | brand |
40
+ | layout | full, icon | full |
41
+ | align | center, logo-left | center |
42
+ | bg, fg, border | #RRGGBB; bg/fg required for custom, border optional | none |
43
+ | appleLogoSrc | URL to your own official Apple logo file | no logo (icon layout shows the text "Apple") |
44
+ | type | button, submit | button |
45
+ | attrs | string attribute names and string values | none |
46
+
47
+ Attributes are sorted by ASCII name after canonical attributes and all values escape
48
+ `& < > " '`. Invalid options, invalid attribute names, event handler attributes and
49
+ overrides of `type`, `class`, `style`, `aria-label` raise `ArgumentError`.
50
+ For boolean HTML attributes use e.g. `attrs: { 'disabled' => '' }`.
51
+
52
+ Official height resolves to the provider's web height; unified text always uses
53
+ `max(14, round(height / 3))`, weight 500. Brand text uses the official size or rounded
54
+ height ratio and weight. Explicit corners are applied without adjustment.
55
+ Custom borders use background/page contrast <1.3; an explicit border overrides it.
56
+ Custom brand logos choose the white-tone palette at relative luminance >0.5.
57
+ The `.slb-stack` and `.slb-row` classes arrange groups. Override the system font with
58
+ `.slb { font-family: inherit; }` after the stylesheet. OAuth is the application's responsibility.
59
+
60
+ Apple artwork is not distributed. Get the official file from
61
+ [Apple Design Resources](https://developer.apple.com/design/resources/#sign-in-with-apple)
62
+ and supply `apple_logo_src`. Files are never tinted; missing URLs produce no logo slot,
63
+ so full-layout text stays centered (icon layout shows the short text "Apple" instead of an empty square).
64
+
65
+ Regenerate from the repository root: `node tools/gen-packages.mjs`.
66
+ Test without Rails:
67
+
68
+ ```sh
69
+ ruby -Ilib -Itest -e 'Dir["test/*_test.rb"].each{|f| require File.expand_path(f)}'
70
+ gem build social_login_buttons.gemspec
71
+ ```
72
+
73
+ Code/tokens/docs: MIT. Provider logos and names are their respective owners' trademarks
74
+ and are excluded from the MIT grant. This package is not affiliated with those companies.
@@ -0,0 +1,59 @@
1
+ /* Generated by tools/gen-packages.mjs from tokens.json. */
2
+ .slb {
3
+ display: inline-flex;
4
+ position: relative;
5
+ box-sizing: border-box;
6
+ align-items: center;
7
+ justify-content: center;
8
+ height: var(--slb-h);
9
+ border-radius: var(--slb-r);
10
+ font-family: -apple-system, "Apple SD Gothic Neo", Roboto, sans-serif;
11
+ font-size: var(--slb-fs);
12
+ font-weight: var(--slb-fw);
13
+ line-height: 1.2;
14
+ background: var(--slb-bg);
15
+ color: var(--slb-fg);
16
+ box-shadow: inset 0 0 0 1px var(--slb-bd);
17
+ border: 0;
18
+ cursor: pointer;
19
+ white-space: nowrap;
20
+ gap: 8px;
21
+ padding: 0 16px;
22
+ word-break: keep-all;
23
+ text-decoration: none;
24
+ vertical-align: middle;
25
+ }
26
+ .slb--full { width: 100%; }
27
+ .slb--icon { width: var(--slb-h); padding: 0; flex-shrink: 0; }
28
+ .slb--logo-left .slb__logo { position: absolute; left: 16px; }
29
+ .slb--icon.slb--logo-left .slb__logo { position: static; }
30
+ .slb__logo {
31
+ display: inline-flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ flex: 0 0 auto;
35
+ color: var(--slb-logo);
36
+ width: var(--slb-logo-size);
37
+ height: var(--slb-logo-size);
38
+ }
39
+ .slb__logo > svg, .slb__logo > img { display: block; width: 100%; height: 100%; flex-shrink: 0; }
40
+ .slb__logo > img { object-fit: contain; }
41
+ .slb__label { min-width: 0; }
42
+ .slb__label--short { font-size: calc(var(--slb-h) * 0.28); }
43
+ .slb--kakao { --slb-logo-size: calc(var(--slb-h) * 0.3); }
44
+ .slb--kakao.slb--icon { --slb-logo-size: calc(var(--slb-h) * 0.46); }
45
+ .slb--naver { --slb-logo-size: 16px; }
46
+ .slb--naver.slb--logo-left { --slb-logo-size: 18px; }
47
+ .slb--naver.slb--icon { --slb-logo-size: 18px; }
48
+ .slb--google { --slb-logo-size: 20px; }
49
+ .slb--apple { --slb-logo-size: var(--slb-h); }
50
+ /* Shift the original out of the clipped box; its zero-blur shadow uses fg exactly. */
51
+ .slb--google.slb--logo-mono .slb__logo { overflow: hidden; }
52
+ .slb--google.slb--logo-mono .slb__logo > img {
53
+ transform: translateX(-100%);
54
+ filter: drop-shadow(var(--slb-logo-size) 0 0 var(--slb-fg));
55
+ }
56
+ .slb-stack { display: flex; flex-direction: column; gap: 10px; }
57
+ .slb-row { display: flex; flex-direction: row; align-items: center; gap: 10px; }
58
+ .slb:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
59
+ .slb:disabled { opacity: 0.5; cursor: not-allowed; }
@@ -0,0 +1,5 @@
1
+ {
2
+ "kakao": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"13 13 22 22\" width=\"22\" height=\"22\"><path fill=\"currentColor\" d=\"M24.0014 14C17.9241 14 13 18.0219 13 22.9825C13 26.1711 15.0368 28.9728 18.1057 30.5656L17.0681 34.5677C17.0295 34.6871 17.0598 34.8151 17.1424 34.9033C17.2029 34.9659 17.2855 35 17.3653 35C17.4341 35 17.5029 34.9772 17.5607 34.9289L22.0196 31.8171C22.661 31.911 23.3215 31.9622 23.9986 31.9622C30.0732 31.9622 35 27.9403 35 22.9797C35 18.0191 30.0759 14 24.0014 14Z\"/></svg>",
3
+ "naver": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 72 72\" width=\"18\" height=\"18\"><g transform=\"translate(-60,132) scale(0.1,-0.1)\"><path fill=\"currentColor\" d=\"M600 960 l0 -360 115 0 115 0 2 193 3 192 133 -192 134 -193 109 0 109 0 0 360 0 360 -115 0 -115 0 -2 -192 -3 -193 -133 193 -134 192 -109 0 -109 0 0 -360z\"/></g></svg>",
4
+ "google": "data:image/png;base64,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"
5
+ }
@@ -0,0 +1,382 @@
1
+ {
2
+ "$schema_note": "소셜 로그인 버튼 디자인 토큰 정본. 값 출처: docs/research/official-guides-2026-11-05.md",
3
+ "version": "0.2.0",
4
+ "order": [
5
+ "kakao",
6
+ "naver",
7
+ "apple",
8
+ "google"
9
+ ],
10
+ "defaults": {
11
+ "height": 48,
12
+ "radius": "official",
13
+ "gap": 10,
14
+ "font": {
15
+ "mode": "unified",
16
+ "size": 16,
17
+ "weight": 500,
18
+ "rule": "size = max(14, round(height / 3))",
19
+ "note": "unified 는 일관성을 위해 모든 버튼을 같은 크기로 맞춘다 — Apple 공식(높이의 43%)·Google 공식(14px)과 다르다. 공식 크기는 font.mode = brand"
20
+ },
21
+ "tone": "brand",
22
+ "logoColor": "brand",
23
+ "layout": "full-width-stack",
24
+ "align": "center",
25
+ "purpose": "auto"
26
+ },
27
+ "options": {
28
+ "radius": [
29
+ "official",
30
+ 0,
31
+ 4,
32
+ 8,
33
+ 12,
34
+ "pill"
35
+ ],
36
+ "radiusRule": "고른 값을 모든 회사에 똑같이 적용한다. 회사별 보정 없음. pill = height / 2",
37
+ "height": [
38
+ "official",
39
+ 44,
40
+ 48,
41
+ 52,
42
+ 56
43
+ ],
44
+ "tone": [
45
+ "brand",
46
+ "white",
47
+ "black",
48
+ "custom"
49
+ ],
50
+ "logoColor": [
51
+ "brand",
52
+ "mono"
53
+ ],
54
+ "layout": [
55
+ "full-width-stack",
56
+ "narrow",
57
+ "icon-row",
58
+ "primary-plus-icons"
59
+ ],
60
+ "align": [
61
+ "center",
62
+ "logo-left"
63
+ ],
64
+ "purpose": [
65
+ "auto",
66
+ "login",
67
+ "continue",
68
+ "signup"
69
+ ],
70
+ "lang": [
71
+ "ko",
72
+ "en"
73
+ ],
74
+ "purposeRule": "auto = 화면에 맞게 고른다 — 가입 화면 signup, 로그인 화면 login, 둘을 겸하면 continue. 한 화면 안의 버튼은 같은 종류로 맞춘다"
75
+ },
76
+ "tone": {
77
+ "white": {
78
+ "light": {
79
+ "bg": "#FFFFFF",
80
+ "fg": "#1F1F1F",
81
+ "border": "#DADCE0"
82
+ },
83
+ "dark": {
84
+ "bg": "#FFFFFF",
85
+ "fg": "#1F1F1F",
86
+ "border": null
87
+ }
88
+ },
89
+ "black": {
90
+ "light": {
91
+ "bg": "#000000",
92
+ "fg": "#FFFFFF",
93
+ "border": null
94
+ },
95
+ "dark": {
96
+ "bg": "#000000",
97
+ "fg": "#FFFFFF",
98
+ "border": "#3A3A3A"
99
+ }
100
+ },
101
+ "custom": {
102
+ "rule": "bg·fg 를 사용자가 정한다. 글자 대비 4.5:1 이상 권장(WCAG AA). bg 가 화면 배경과 대비 1.3 미만이면 border = 흰 화면 #DADCE0 / 검정 화면 #3A3A3A"
103
+ },
104
+ "brandLogoOnTone": {
105
+ "kakao": {
106
+ "white": "#191919",
107
+ "black": "#FEE500"
108
+ },
109
+ "naver": {
110
+ "white": "#03A94D",
111
+ "black": "#05AC4F"
112
+ },
113
+ "apple": {
114
+ "white": "#000000",
115
+ "black": "#FFFFFF"
116
+ },
117
+ "google": {
118
+ "white": "full-color",
119
+ "black": "full-color"
120
+ },
121
+ "_rule": "tone 이 white/black/custom 일 때만 쓴다. tone=brand 면 providers.<회사>.brand.<theme>.logo (없으면 그 fg, Google G 는 원래 색)"
122
+ }
123
+ },
124
+ "providers": {
125
+ "kakao": {
126
+ "name": {
127
+ "ko": "카카오",
128
+ "en": "Kakao"
129
+ },
130
+ "brand": {
131
+ "light": {
132
+ "bg": "#FEE500",
133
+ "fg": "#191919",
134
+ "logo": "#191919"
135
+ },
136
+ "dark": {
137
+ "bg": "#FEE500",
138
+ "fg": "#191919",
139
+ "logo": "#191919"
140
+ }
141
+ },
142
+ "officialHeight": 46,
143
+ "officialRadius": 8,
144
+ "officialFont": {
145
+ "sizeRatio": 0.326,
146
+ "weight": 500
147
+ },
148
+ "logo": {
149
+ "file": "assets/kakao-symbol.svg",
150
+ "sizeRatio": 0.3,
151
+ "iconSizeRatio": 0.46
152
+ },
153
+ "padding": {
154
+ "left": 16,
155
+ "gap": 8
156
+ },
157
+ "label": {
158
+ "ko": {
159
+ "login": "카카오 로그인",
160
+ "continue": "카카오로 계속하기",
161
+ "signup": "카카오로 가입하기",
162
+ "short": "로그인"
163
+ },
164
+ "en": {
165
+ "login": "Login with Kakao",
166
+ "continue": "Continue with Kakao",
167
+ "signup": "Sign up with Kakao",
168
+ "short": "Login"
169
+ }
170
+ },
171
+ "notes": [
172
+ "가이드 본문은 모서리 12px·글자 #000000 85%, 공식 애셋은 8px·#191919 — 애셋 값을 기본으로 씀"
173
+ ]
174
+ },
175
+ "naver": {
176
+ "name": {
177
+ "ko": "네이버",
178
+ "en": "NAVER"
179
+ },
180
+ "brand": {
181
+ "light": {
182
+ "bg": "#03A94D",
183
+ "fg": "#FFFFFF",
184
+ "logo": "#FFFFFF"
185
+ },
186
+ "dark": {
187
+ "bg": "#05AC4F",
188
+ "fg": "#FFFFFF",
189
+ "logo": "#FFFFFF"
190
+ },
191
+ "alt_light": {
192
+ "bg": "#FFFFFF",
193
+ "fg": "#000000",
194
+ "logo": "#03A94D",
195
+ "border": "#D9D9D9"
196
+ },
197
+ "alt_dark": {
198
+ "bg": "#1F1F1F",
199
+ "fg": "#FFFFFF",
200
+ "logo": "#05AC4F",
201
+ "border": "#414141"
202
+ }
203
+ },
204
+ "officialHeight": 48,
205
+ "officialRadius": 8,
206
+ "officialFont": {
207
+ "sizeRatio": 0.333,
208
+ "weight": 600
209
+ },
210
+ "logo": {
211
+ "file": "assets/naver-n.svg",
212
+ "size": {
213
+ "center": 16,
214
+ "logoLeft": 18,
215
+ "icon": 18
216
+ }
217
+ },
218
+ "padding": {
219
+ "left": 20,
220
+ "gap": 8
221
+ },
222
+ "label": {
223
+ "ko": {
224
+ "login": "네이버 로그인",
225
+ "continue": "네이버로 계속하기",
226
+ "signup": "네이버로 가입하기",
227
+ "short": "로그인"
228
+ },
229
+ "en": {
230
+ "login": "Log in with NAVER",
231
+ "continue": "Continue with NAVER",
232
+ "signup": "Sign up with NAVER",
233
+ "short": "Log in"
234
+ }
235
+ },
236
+ "notes": [
237
+ "로그인 버튼 지정색은 #03A94D (브랜드색 #03C75A 아님)"
238
+ ]
239
+ },
240
+ "apple": {
241
+ "name": {
242
+ "ko": "Apple",
243
+ "en": "Apple"
244
+ },
245
+ "brand": {
246
+ "light": {
247
+ "bg": "#000000",
248
+ "fg": "#FFFFFF",
249
+ "logo": "#FFFFFF"
250
+ },
251
+ "dark": {
252
+ "bg": "#FFFFFF",
253
+ "fg": "#000000",
254
+ "logo": "#000000"
255
+ },
256
+ "alt_light": {
257
+ "bg": "#FFFFFF",
258
+ "fg": "#000000",
259
+ "logo": "#000000",
260
+ "border": "#000000"
261
+ }
262
+ },
263
+ "officialHeight": 44,
264
+ "officialRadius": 6,
265
+ "officialFont": {
266
+ "sizeRatio": 0.43,
267
+ "weight": 500
268
+ },
269
+ "minWidth": 140,
270
+ "minHeight": 30,
271
+ "logo": {
272
+ "file": null,
273
+ "redistributable": false,
274
+ "download": "https://developer.apple.com/design/resources/#sign-in-with-apple",
275
+ "instructions": "Apple Design Resources 에서 「Sign in with Apple」 로고(Left-aligned·Logo-only, SVG)를 받아 프로젝트에 넣는다. 로고 파일 높이 = 버튼 높이, 자르거나 세로 여백을 더하지 않는다. iOS 는 ASAuthorizationAppleIDButton, 웹은 Sign in with Apple JS 버튼을 써도 된다.",
276
+ "missingFallback": "파일이 없으면 로고 자리를 그리지 않고(빈칸 금지) 글자만 가운데. 웹은 img onerror 로 숨김, 앱은 에셋 존재 확인",
277
+ "noTint": "공식 로고 파일에는 배경 사각형이 들어 있다. 마스크·colorFilter·fill 로 색을 입히지 말고 버튼 배경에 맞는 파일을 그대로 쓴다 — 검정 버튼 = White 파일, 흰 버튼 = Black 파일"
278
+ },
279
+ "label": {
280
+ "ko": {
281
+ "login": "Apple로 로그인",
282
+ "continue": "Apple로 계속하기",
283
+ "signup": "Apple로 등록",
284
+ "short": "로그인"
285
+ },
286
+ "en": {
287
+ "login": "Sign in with Apple",
288
+ "continue": "Continue with Apple",
289
+ "signup": "Sign up with Apple",
290
+ "short": "Sign in"
291
+ }
292
+ },
293
+ "notes": [
294
+ "로고는 재배포 금지라 이 저장소에 없다",
295
+ "로고와 글자는 둘 다 검정 또는 둘 다 흰색"
296
+ ]
297
+ },
298
+ "google": {
299
+ "name": {
300
+ "ko": "Google",
301
+ "en": "Google"
302
+ },
303
+ "brand": {
304
+ "light": {
305
+ "bg": "#FFFFFF",
306
+ "fg": "#1F1F1F",
307
+ "border": "#747775"
308
+ },
309
+ "dark": {
310
+ "bg": "#131314",
311
+ "fg": "#E3E3E3",
312
+ "border": "#8E918F"
313
+ },
314
+ "neutral": {
315
+ "bg": "#F2F2F2",
316
+ "fg": "#1F1F1F",
317
+ "border": null
318
+ }
319
+ },
320
+ "officialHeight": {
321
+ "web": 40,
322
+ "android": 40,
323
+ "ios": 44
324
+ },
325
+ "officialRadius": 4,
326
+ "officialFont": {
327
+ "family": "Google Sans Medium",
328
+ "size": 14,
329
+ "lineHeight": 20,
330
+ "weight": 500
331
+ },
332
+ "logo": {
333
+ "file": "assets/google-g.svg",
334
+ "png": "assets/google-g@{1,2,3,4}x.png",
335
+ "size": 20,
336
+ "note": "SVG 는 브라우저용(그라데이션). Flutter·네이티브는 PNG 를 쓴다"
337
+ },
338
+ "padding": {
339
+ "web": [
340
+ 12,
341
+ 10,
342
+ 12
343
+ ],
344
+ "android": [
345
+ 12,
346
+ 10,
347
+ 12
348
+ ],
349
+ "ios": [
350
+ 16,
351
+ 12,
352
+ 16
353
+ ]
354
+ },
355
+ "label": {
356
+ "ko": {
357
+ "login": "Google 계정으로 로그인",
358
+ "continue": "Google 계정으로 계속하기",
359
+ "signup": "Google 계정으로 가입",
360
+ "short": "로그인"
361
+ },
362
+ "en": {
363
+ "login": "Sign in with Google",
364
+ "continue": "Continue with Google",
365
+ "signup": "Sign up with Google",
366
+ "short": "Sign in"
367
+ }
368
+ },
369
+ "notes": [
370
+ "「Google」 단독 문구 금지",
371
+ "G 로고 크기·색 변경 금지(가이드)"
372
+ ]
373
+ }
374
+ },
375
+ "feedback": {
376
+ "issues": "https://github.com/AI-Dico/social-login-buttons/issues/new",
377
+ "ask": "적용하다가 공식 가이드와 다른 점, 값이 틀린 곳, 플랫폼에서 구현이 어려운 점을 발견하면 이슈로 남겨 주세요(화면·스택·고른 옵션·스크린샷)."
378
+ },
379
+ "platformNotes": {
380
+ "flutter": "글자 스타일은 Theme.of(context).textTheme.labelLarge?.copyWith(...) 로 앱 테마를 이어받는다. styleFrom(textStyle: TextStyle(...)) 로 새로 만들면 앱 글꼴이 빠진다"
381
+ }
382
+ }
@@ -0,0 +1,8 @@
1
+ module SocialLoginButtons
2
+ module Helper
3
+ def social_login_button(provider, **options)
4
+ html = SocialLoginButtons.render(options.merge(provider: provider))
5
+ html.respond_to?(:html_safe) ? html.html_safe : html
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ module SocialLoginButtons
2
+ class Railtie < Rails::Railtie
3
+ initializer 'social_login_buttons.helpers' do
4
+ ActiveSupport.on_load(:action_view) { include SocialLoginButtons::Helper }
5
+ end
6
+
7
+ initializer 'social_login_buttons.assets' do |app|
8
+ if app.config.respond_to?(:assets)
9
+ app.config.assets.paths << File.expand_path('../../app/assets/stylesheets', __dir__)
10
+ if app.config.assets.respond_to?(:precompile)
11
+ app.config.assets.precompile += ['social_login_buttons.css']
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,137 @@
1
+ require 'json'
2
+
3
+ module SocialLoginButtons
4
+ class Renderer
5
+ DATA_PATH = File.expand_path('data', __dir__)
6
+ TOKENS = JSON.parse(File.read(File.join(DATA_PATH, 'tokens.json')))
7
+ LOGOS = JSON.parse(File.read(File.join(DATA_PATH, 'logos.json')))
8
+ DEFAULTS = {
9
+ 'purpose' => 'continue', 'lang' => 'ko', 'tone' => 'brand', 'theme' => 'light',
10
+ 'corner' => 'official', 'height' => 48, 'font' => 'unified', 'logoColor' => 'brand',
11
+ 'layout' => 'full', 'align' => 'center', 'type' => 'button'
12
+ }.freeze
13
+ CHOICES = {
14
+ 'purpose' => %w[login continue signup short], 'lang' => %w[ko en],
15
+ 'tone' => %w[brand white black custom], 'theme' => %w[light dark],
16
+ 'font' => %w[unified brand], 'logoColor' => %w[brand mono],
17
+ 'layout' => %w[full icon], 'align' => %w[center logo-left], 'type' => %w[button submit]
18
+ }.freeze
19
+
20
+ def self.render(options = {})
21
+ new.render(options)
22
+ end
23
+
24
+ def render(options)
25
+ normalized = options.to_h.transform_keys { |key| key.to_s.gsub(/_([a-z])/) { Regexp.last_match(1).upcase } }
26
+ o = DEFAULTS.merge(normalized).transform_values { |v| v.is_a?(Symbol) ? v.to_s : v }
27
+ p = TOKENS['providers'][o['provider']]
28
+ raise ArgumentError, 'Invalid provider' unless p
29
+ CHOICES.each { |key, values| raise ArgumentError, "Invalid #{key}" unless values.include?(o[key]) }
30
+ h = o['height'] == 'official' ? p['officialHeight'] : o['height']
31
+ h = h['web'] if h.is_a?(Hash)
32
+ unless h.is_a?(Numeric) && h.finite? && h.positive?
33
+ raise ArgumentError, 'Height must be a positive finite number'
34
+ end
35
+ corner = o['corner']
36
+ corner = corner.to_i if corner.is_a?(String) && /\A(?:0|4|8|12)\z/.match?(corner)
37
+ raise ArgumentError, 'Invalid corner' unless ['official', 0, 4, 8, 12, 'pill'].include?(corner)
38
+ r = corner == 'official' ? p['officialRadius'] : corner == 'pill' ? h / 2.0 : corner
39
+ fs = if o['font'] == 'unified'
40
+ [14, (h / 3.0).round].max
41
+ else
42
+ p['officialFont']['size'] || (h * p['officialFont']['sizeRatio']).round
43
+ end
44
+ fw = o['font'] == 'unified' ? 500 : p['officialFont']['weight']
45
+ if o['tone'] == 'custom'
46
+ bg = color(o['bg'])
47
+ fg = color(o['fg'])
48
+ a = luminance(bg)
49
+ b = o['theme'] == 'light' ? 1 : 0
50
+ bd = if o['border']
51
+ color(o['border'])
52
+ elsif ([a, b].max + 0.05) / ([a, b].min + 0.05) < 1.3
53
+ o['theme'] == 'light' ? '#DADCE0' : '#3A3A3A'
54
+ else
55
+ 'transparent'
56
+ end
57
+ else
58
+ palette = o['tone'] == 'brand' ? p['brand'][o['theme']] : TOKENS['tone'][o['tone']][o['theme']]
59
+ bg = color(palette['bg'])
60
+ fg = color(palette['fg'])
61
+ bd = palette['border'] ? color(palette['border']) : 'transparent'
62
+ end
63
+ logo = fg
64
+ if o['logoColor'] == 'brand'
65
+ tone = o['tone'] == 'custom' ? (luminance(bg) > 0.5 ? 'white' : 'black') : o['tone']
66
+ candidate = tone == 'brand' ? p['brand'][o['theme']]['logo'] : TOKENS['tone']['brandLogoOnTone'][o['provider']][tone]
67
+ logo = color(candidate) if candidate && candidate != 'full-color'
68
+ end
69
+ label = p['label'][o['lang']][o['purpose']]
70
+ classes = ['slb', "slb--#{o['provider']}", "slb--tone-#{o['tone']}", "slb--#{o['theme']}", "slb--#{o['layout']}", "slb--#{o['align']}"]
71
+ classes << 'slb--logo-mono' if o['logoColor'] == 'mono'
72
+ style = "--slb-h:#{number(h)}px;--slb-r:#{number(r)}px;--slb-fs:#{number(fs)}px;--slb-fw:#{number(fw)};--slb-bg:#{bg};--slb-fg:#{fg};--slb-bd:#{bd};--slb-logo:#{logo}"
73
+ extra = ''
74
+ unless o['attrs'].nil?
75
+ raise ArgumentError, 'attrs must be a string map' unless o['attrs'].is_a?(Hash) && o['attrs'].keys.all? { |key| key.is_a?(String) }
76
+ o['attrs'].sort.each do |key, value|
77
+ if !/\A[A-Za-z_:][A-Za-z0-9_.:-]*\z/.match?(key) || /\A(?:type|class|style|aria-label)\z/i.match?(key) || /\Aon/i.match?(key)
78
+ raise ArgumentError, "Invalid or reserved attribute: #{key}"
79
+ end
80
+ raise ArgumentError, 'Attribute values must be strings' unless value.is_a?(String)
81
+ extra += " #{key}=\"#{escape(value)}\""
82
+ end
83
+ end
84
+ artwork = ''
85
+ if o['provider'] == 'apple'
86
+ src = o['appleLogoSrc']
87
+ unless src.nil? || src == ''
88
+ raise ArgumentError, 'appleLogoSrc must be a string' unless src.is_a?(String)
89
+ artwork = "<img src=\"#{escape(src)}\" alt=\"\">"
90
+ end
91
+ elsif o['provider'] == 'google'
92
+ artwork = "<img src=\"#{escape(LOGOS['google'])}\" alt=\"\">"
93
+ else
94
+ artwork = LOGOS[o['provider']]
95
+ end
96
+ logo_html = artwork.empty? ? '' : "<span class=\"slb__logo\" aria-hidden=\"true\">#{artwork}</span>"
97
+ # Without a logo (Apple with no appleLogoSrc) an icon button would be an empty square, so show the short name.
98
+ label_html = if o['layout'] != 'icon'
99
+ "<span class=\"slb__label\">#{escape(label)}</span>"
100
+ elsif artwork.empty?
101
+ "<span class=\"slb__label slb__label--short\">#{escape(p['name'][o['lang']])}</span>"
102
+ else
103
+ ''
104
+ end
105
+ "<button type=\"#{o['type']}\" class=\"#{classes.join(' ')}\" style=\"#{style}\" aria-label=\"#{escape(label)}\"#{extra}>#{logo_html}#{label_html}</button>"
106
+ end
107
+
108
+ private
109
+
110
+ def number(value)
111
+ return value.to_i.to_s if value.to_i == value && value.abs <= 9_007_199_254_740_991
112
+ result = value.to_f.to_s.sub(/\.0e/, 'e').sub(/e(-?)0+/, 'e\1')
113
+ return result unless result.include?('e')
114
+ mantissa, exponent = result.split('e')
115
+ exponent = exponent.to_i
116
+ return "#{mantissa}e#{exponent >= 0 ? '+' : ''}#{exponent}" if exponent < -6 || exponent >= 21
117
+ digits = mantissa.delete('.')
118
+ point = exponent + 1
119
+ return "0.#{'0' * -point}#{digits}" if point <= 0
120
+ point >= digits.size ? digits.ljust(point, '0') : "#{digits[0...point]}.#{digits[point..]}"
121
+ end
122
+
123
+ def escape(value)
124
+ value.to_s.gsub(/[&<>"']/) { |c| { '&' => '&amp;', '<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "'" => '&#39;' }[c] }
125
+ end
126
+
127
+ def color(value)
128
+ raise ArgumentError, 'Expected #RRGGBB color' unless value.is_a?(String) && /\A#[\da-f]{6}\z/i.match?(value)
129
+ value.upcase
130
+ end
131
+
132
+ def luminance(hex)
133
+ c = hex[1..].scan(/../).map { |v| v.to_i(16) / 255.0 }.map { |v| v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055)**2.4 }
134
+ c[0] * 0.2126 + c[1] * 0.7152 + c[2] * 0.0722
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,3 @@
1
+ module SocialLoginButtons
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'social_login_buttons/version'
2
+ require_relative 'social_login_buttons/renderer'
3
+ require_relative 'social_login_buttons/helper'
4
+
5
+ module SocialLoginButtons
6
+ def self.render(options = {})
7
+ Renderer.render(options)
8
+ end
9
+ end
10
+
11
+ require_relative 'social_login_buttons/railtie' if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: social_login_buttons
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dico (AI-Dico)
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-11-05 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Render Kakao, NAVER, Apple and Google buttons with HTML identical to
13
+ the companion npm package.
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - LICENSE
19
+ - README.md
20
+ - app/assets/stylesheets/social_login_buttons.css
21
+ - lib/social_login_buttons.rb
22
+ - lib/social_login_buttons/data/logos.json
23
+ - lib/social_login_buttons/data/tokens.json
24
+ - lib/social_login_buttons/helper.rb
25
+ - lib/social_login_buttons/railtie.rb
26
+ - lib/social_login_buttons/renderer.rb
27
+ - lib/social_login_buttons/version.rb
28
+ homepage: https://github.com/AI-Dico/social-login-buttons
29
+ licenses:
30
+ - MIT
31
+ metadata:
32
+ source_code_uri: https://github.com/AI-Dico/social-login-buttons
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3.1'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubygems_version: 3.6.2
48
+ specification_version: 4
49
+ summary: Token-based social login buttons for Ruby and Rails
50
+ test_files: []